diff --git a/internal/application/certification/certification_application_service.go b/internal/application/certification/certification_application_service.go index 6f05b8f..a6341b7 100644 --- a/internal/application/certification/certification_application_service.go +++ b/internal/application/certification/certification_application_service.go @@ -45,6 +45,9 @@ type CertificationApplicationService interface { // AdminCompleteCertificationWithoutContract 管理员代用户完成认证(暂不关联合同) AdminCompleteCertificationWithoutContract(ctx context.Context, cmd *commands.AdminCompleteCertificationCommand) (*responses.CertificationResponse, error) + // AdminResetForResignContract 管理员触发重新认证(回退到提交企业信息,保留钱包/API Key/历史合同,不改 is_certified) + AdminResetForResignContract(ctx context.Context, cmd *commands.AdminResetForResignContractCommand) (*responses.CertificationResponse, error) + // AdminListSubmitRecords 管理端分页查询企业信息提交记录 AdminListSubmitRecords(ctx context.Context, query *queries.AdminListSubmitRecordsQuery) (*responses.AdminSubmitRecordsListResponse, error) // AdminGetSubmitRecordByID 管理端获取单条提交记录详情 diff --git a/internal/application/certification/certification_application_service_impl.go b/internal/application/certification/certification_application_service_impl.go index 960effe..9cd90fe 100644 --- a/internal/application/certification/certification_application_service_impl.go +++ b/internal/application/certification/certification_application_service_impl.go @@ -1003,6 +1003,48 @@ func (s *CertificationApplicationServiceImpl) AdminCompleteCertificationWithoutC return response, nil } +// AdminResetForResignContract 管理员触发重新认证(回退到提交企业信息) +// 回退到 pending,用户可重新填写企业信息并走完整流程;不改 is_certified,不动钱包/API Key/历史合同 +func (s *CertificationApplicationServiceImpl) AdminResetForResignContract( + ctx context.Context, + cmd *commands.AdminResetForResignContractCommand, +) (*responses.CertificationResponse, error) { + s.logger.Info("管理员触发重新认证", + zap.String("admin_id", cmd.AdminID), + zap.String("user_id", cmd.UserID), + zap.String("reason", cmd.Reason), + ) + + if strings.TrimSpace(cmd.UserID) == "" { + return nil, fmt.Errorf("用户ID不能为空") + } + if strings.TrimSpace(cmd.Reason) == "" { + return nil, fmt.Errorf("请填写操作原因") + } + + cert, err := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID) + if err != nil { + return nil, fmt.Errorf("加载认证信息失败: %s", err.Error()) + } + + if err := cert.ResetForResignContract(cmd.AdminID, fmt.Sprintf("管理员触发重新认证:%s", cmd.Reason)); err != nil { + return nil, err + } + + if err := s.aggregateService.SaveCertification(ctx, cert); err != nil { + return nil, fmt.Errorf("保存认证信息失败: %s", err.Error()) + } + + 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), + zap.String("status", string(cert.Status)), + ) + return response, nil +} + // AdminListSubmitRecords 管理端分页查询企业信息提交记录 func (s *CertificationApplicationServiceImpl) AdminListSubmitRecords( ctx context.Context, diff --git a/internal/application/certification/dto/commands/certification_commands.go b/internal/application/certification/dto/commands/certification_commands.go index d5a40bc..2ee3b0a 100644 --- a/internal/application/certification/dto/commands/certification_commands.go +++ b/internal/application/certification/dto/commands/certification_commands.go @@ -85,6 +85,13 @@ type AdminCompleteCertificationCommand struct { // 备注信息,用于记录后台操作原因 Reason string `json:"reason" validate:"required"` } + +// AdminResetForResignContractCommand 管理员触发用户重新认证(回退到提交企业信息,保留钱包/API Key/历史记录) +type AdminResetForResignContractCommand struct { + AdminID string `json:"-"` + UserID string `json:"user_id" validate:"required"` + Reason string `json:"reason" validate:"required,min=2,max=500"` +} // ForceTransitionStatusCommand 强制状态转换命令(管理员) type ForceTransitionStatusCommand struct { CertificationID string `json:"certification_id" validate:"required"` diff --git a/internal/domains/certification/entities/certification.go b/internal/domains/certification/entities/certification.go index a24ba74..c3f0a05 100644 --- a/internal/domains/certification/entities/certification.go +++ b/internal/domains/certification/entities/certification.go @@ -552,6 +552,57 @@ func (c *Certification) ResetAfterSignTaskCreateFailed() error { return nil } +// ResetForResignContract 管理员触发重新认证:回退到 pending,用户可重新填写企业信息并走完整入驻流程 +// 不改动用户域认证标记、钱包、API Key、历史合同记录 +func (c *Certification) ResetForResignContract(actorID, reason string) error { + switch c.Status { + case enums.StatusCompleted, enums.StatusContractSigned, enums.StatusContractApplied, + enums.StatusContractRejected, enums.StatusContractExpired, + enums.StatusEnterpriseVerified, enums.StatusInfoSubmitted, enums.StatusInfoPendingReview: + // 允许从已进入流程的各状态重置,供用户重新填写企业信息 + default: + if c.Status == enums.StatusPending { + return fmt.Errorf("当前已是待提交企业信息状态,无需重置") + } + return fmt.Errorf("当前状态 %s 不允许重新认证", enums.GetStatusName(c.Status)) + } + + oldStatus := c.Status + now := time.Now() + + c.Status = enums.StatusPending + c.SignPlatform = "" + c.AuthFlowID = "" + c.AuthURL = "" + c.ContractCode = "" + c.ContractFileID = "" + c.EsignFlowID = "" + c.ContractURL = "" + c.ContractSignURL = "" + c.InfoSubmittedAt = nil + c.EnterpriseVerifiedAt = nil + c.ContractAppliedAt = nil + c.ContractSignedAt = nil + c.CompletedAt = nil + c.ContractFileCreatedAt = nil + c.RetryCount = 0 + c.clearFailureInfo() + c.updateTransitionAudit(enums.ActorTypeAdmin, actorID) + c.UpdatedAt = now + + c.addDomainEvent(&CertificationStatusChangedEvent{ + CertificationID: c.ID, + UserID: c.UserID, + FromStatus: oldStatus, + ToStatus: enums.StatusPending, + Actor: enums.ActorTypeAdmin, + ActorID: actorID, + Reason: reason, + TransitionedAt: now, + }) + return nil +} + // SetSignPlatform 设置签署平台(选定后锁定) func (c *Certification) SetSignPlatform(platform enums.SignPlatform) error { if !enums.IsValidSignPlatform(platform) { diff --git a/internal/infrastructure/http/handlers/certification_handler.go b/internal/infrastructure/http/handlers/certification_handler.go index 6af4c6b..2e48933 100644 --- a/internal/infrastructure/http/handlers/certification_handler.go +++ b/internal/infrastructure/http/handlers/certification_handler.go @@ -559,6 +559,46 @@ func (h *CertificationHandler) AdminCompleteCertificationWithoutContract(c *gin. h.response.Success(c, result, "代用户完成认证成功") } +// AdminResetForResignContract 管理员触发重新认证 +// @Summary 管理员触发重新认证 +// @Description 将认证回退到 pending,用户可重新填写企业信息并走完整入驻流程;保留钱包、API Key、历史合同与 is_certified +// @Tags 认证管理 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body commands.AdminResetForResignContractCommand true "管理员重新认证请求" +// @Success 200 {object} responses.CertificationResponse "操作成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 403 {object} map[string]interface{} "权限不足" +// @Router /api/v1/certifications/admin/reset-for-resign-contract [post] +func (h *CertificationHandler) AdminResetForResignContract(c *gin.Context) { + adminID := h.getCurrentUserID(c) + if adminID == "" { + h.response.Unauthorized(c, "用户未登录") + return + } + + var cmd commands.AdminResetForResignContractCommand + if err := h.validator.BindAndValidate(c, &cmd); err != nil { + return + } + cmd.AdminID = adminID + + result, err := h.appService.AdminResetForResignContract(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, "已重置为重新认证,请通知用户前往企业入驻重新填写企业信息") +} + // AdminListSubmitRecords 管理端分页查询企业信息提交记录 // @Summary 管理端企业审核列表 // @Tags 认证管理 diff --git a/internal/infrastructure/http/routes/certification_routes.go b/internal/infrastructure/http/routes/certification_routes.go index 28785b1..b24ffa3 100644 --- a/internal/infrastructure/http/routes/certification_routes.go +++ b/internal/infrastructure/http/routes/certification_routes.go @@ -90,6 +90,7 @@ func (r *CertificationRoutes) Register(router *http.GinRouter) { adminGroup.Use(r.admin.Handle()) { adminGroup.POST("/transition-status", r.handler.AdminTransitionCertificationStatus) + adminGroup.POST("/reset-for-resign-contract", r.handler.AdminResetForResignContract) } adminCertGroup := adminGroup.Group("/submit-records") {