This commit is contained in:
Mrx
2026-03-19 13:23:48 +08:00
parent faf4b7f6a7
commit d837624c0a
12 changed files with 222 additions and 171 deletions

View File

@@ -41,10 +41,12 @@ type CertificationApplicationService interface {
AdminListSubmitRecords(ctx context.Context, query *queries.AdminListSubmitRecordsQuery) (*responses.AdminSubmitRecordsListResponse, error)
// AdminGetSubmitRecordByID 管理端获取单条提交记录详情
AdminGetSubmitRecordByID(ctx context.Context, recordID string) (*responses.AdminSubmitRecordDetail, error)
// AdminApproveSubmitRecord 管理端审核通过
// AdminApproveSubmitRecord 管理端审核通过(按提交记录 ID
AdminApproveSubmitRecord(ctx context.Context, recordID, adminID, remark string) error
// AdminRejectSubmitRecord 管理端审核拒绝
// AdminRejectSubmitRecord 管理端审核拒绝(按提交记录 ID
AdminRejectSubmitRecord(ctx context.Context, recordID, adminID, remark string) error
// AdminTransitionCertificationStatus 管理端按用户变更认证状态以状态机为准info_submitted=通过 / info_rejected=拒绝)
AdminTransitionCertificationStatus(ctx context.Context, cmd *commands.AdminTransitionCertificationStatusCommand) error
// ================ e签宝回调处理 ================

View File

@@ -109,10 +109,13 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
s.logger.Info("开始提交企业信息",
zap.String("user_id", cmd.UserID))
// 0. 若该用户已有待审核的提交记录,则不允许重复提交
// 0. 若该用户已有待审核(认证状态仍在待审核),则不允许重复提交
latestRecord, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cmd.UserID)
if err == nil && latestRecord != nil && latestRecord.ManualReviewStatus == "pending" {
return nil, fmt.Errorf("您已有待审核的提交,请等待管理员审核后再操作")
if err == nil && latestRecord != nil {
cert, loadErr := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID)
if loadErr == nil && cert != nil && cert.Status == enums.StatusInfoPendingReview {
return nil, fmt.Errorf("您已有待审核的提交,请等待管理员审核后再操作")
}
}
// 1.5 插入企业信息提交记录(包含扩展字段)
@@ -259,16 +262,7 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
return fmt.Errorf("加载认证信息失败: %s", err.Error())
}
// 已是「已提交企业信息」:说明已通过人工审核,直接返回当前认证数据,前端可刷新到企业认证步骤
if cert.Status == enums.StatusInfoSubmitted {
response = s.convertToResponse(cert)
response.Metadata = map[string]interface{}{
"next_action": "您已通过审核,请完成企业认证步骤",
}
return nil
}
// 4. 提交企业信息进入人工审核(不调用 e签宝不生成认证链接
// 4. 提交企业信息进入人工审核(不在此处推断审核是否通过)
err = cert.SubmitEnterpriseInfoForReview(enterpriseInfo)
if err != nil {
return fmt.Errorf("提交企业信息失败: %s", err.Error())
@@ -303,6 +297,25 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
return response, nil
}
// 审核状态检查(步骤二)
// 规则企业信息提交成功后进入待审核审核通过后才允许进行企业认证确认ConfirmAuth
func (s *CertificationApplicationServiceImpl) checkAuditStatus(ctx context.Context, cert *entities.Certification) error {
switch cert.Status {
case enums.StatusInfoSubmitted,
enums.StatusEnterpriseVerified,
enums.StatusContractApplied,
enums.StatusContractSigned,
enums.StatusCompleted:
return nil
case enums.StatusInfoPendingReview:
return fmt.Errorf("企业信息已提交,正在审核中")
case enums.StatusInfoRejected:
return fmt.Errorf("企业信息审核未通过")
default:
return fmt.Errorf("认证状态不正确,当前状态: %s", enums.GetStatusName(cert.Status))
}
}
// ConfirmAuth 确认认证状态
func (s *CertificationApplicationServiceImpl) ConfirmAuth(
ctx context.Context,
@@ -314,9 +327,9 @@ func (s *CertificationApplicationServiceImpl) ConfirmAuth(
return nil, fmt.Errorf("加载认证信息失败: %s", err.Error())
}
// 企业认证
if cert.Status != enums.StatusInfoSubmitted {
return nil, fmt.Errorf("认证状态不正确,当前状态: %s", enums.GetStatusName(cert.Status))
// 步骤二:审核状态检查(审核通过后才能进入企业认证确认)
if err := s.checkAuditStatus(ctx, cert); err != nil {
return nil, err
}
record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID)
if err != nil {
@@ -730,12 +743,12 @@ func (s *CertificationApplicationServiceImpl) AdminListSubmitRecords(
query.Page = 1
}
filter := repositories.ListSubmitRecordsFilter{
Page: query.Page,
PageSize: query.PageSize,
ManualReviewStatus: query.ManualReviewStatus,
CompanyName: query.CompanyName,
LegalPersonPhone: query.LegalPersonPhone,
LegalPersonName: query.LegalPersonName,
Page: query.Page,
PageSize: query.PageSize,
CertificationStatus: query.CertificationStatus,
CompanyName: query.CompanyName,
LegalPersonPhone: query.LegalPersonPhone,
LegalPersonName: query.LegalPersonName,
}
result, err := s.enterpriseInfoSubmitRecordRepo.List(ctx, filter)
if err != nil {
@@ -755,8 +768,6 @@ func (s *CertificationApplicationServiceImpl) AdminListSubmitRecords(
LegalPersonName: r.LegalPersonName,
SubmitAt: r.SubmitAt,
Status: r.Status,
ManualReviewStatus: r.ManualReviewStatus,
ManualReviewedAt: r.ManualReviewedAt,
CertificationStatus: certStatus,
})
}
@@ -805,10 +816,6 @@ func (s *CertificationApplicationServiceImpl) AdminGetSubmitRecordByID(ctx conte
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,
@@ -821,17 +828,12 @@ func (s *CertificationApplicationServiceImpl) AdminApproveSubmitRecord(ctx conte
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)
}
// 兼容线上脏数据:认证已进入「已提交企业信息」或更后续状态,但提交记录仍是 pending
// 此时无需再走「待审核->通过」的状态机,只需要把提交记录补齐为 approved避免管理端无法操作。
// 说明:后续状态(如已企业认证/合同状态/完成)都意味着企业信息审核已经通过。
// 幂等:认证已进入「已提交企业信息」或更后续状态,说明已通过审核,无需重复操作
switch cert.Status {
case enums.StatusInfoSubmitted,
enums.StatusEnterpriseVerified,
@@ -840,31 +842,11 @@ func (s *CertificationApplicationServiceImpl) AdminApproveSubmitRecord(ctx conte
enums.StatusCompleted,
enums.StatusContractRejected,
enums.StatusContractExpired:
record.MarkManualApproved(adminID, remark)
if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil {
return fmt.Errorf("保存提交记录失败: %w", err)
}
s.logger.Info("已补齐提交记录人工审核为通过(认证已进入后续状态)",
zap.String("record_id", recordID),
zap.String("admin_id", adminID),
zap.String("user_id", record.UserID),
zap.String("certification_status", string(cert.Status)),
)
return nil
}
// 兼容线上脏数据:提交记录已落库但当时事务失败导致认证仍为「待认证」,先同步为待审核再执行通过
if cert.Status != enums.StatusInfoPendingReview {
if err := s.syncCertToPendingReviewIfRecordPending(ctx, cert, record); err != nil {
return err
}
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))
}
return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status))
}
enterpriseInfo := &certification_value_objects.EnterpriseInfo{
CompanyName: record.CompanyName,
@@ -886,10 +868,6 @@ func (s *CertificationApplicationServiceImpl) AdminApproveSubmitRecord(ctx conte
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)
}
@@ -909,29 +887,24 @@ func (s *CertificationApplicationServiceImpl) AdminRejectSubmitRecord(ctx contex
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 {
if err := s.syncCertToPendingReviewIfRecordPending(ctx, cert, record); err != nil {
return err
}
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))
}
// 幂等:认证已处于拒绝或后续状态,无需重复拒绝
switch cert.Status {
case enums.StatusInfoRejected,
enums.StatusEnterpriseVerified,
enums.StatusContractApplied,
enums.StatusContractSigned,
enums.StatusCompleted,
enums.StatusContractRejected,
enums.StatusContractExpired:
return nil
}
record.MarkManualRejected(adminID, remark)
if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil {
return fmt.Errorf("保存提交记录失败: %w", err)
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)
@@ -943,35 +916,75 @@ func (s *CertificationApplicationServiceImpl) AdminRejectSubmitRecord(ctx contex
return nil
}
// ================ 辅助方法 ================
// syncCertToPendingReviewIfRecordPending 兼容历史脏数据:当认证为「待认证」或「已拒绝」且存在待审核提交记录时,
// 用该记录的企业信息把认证同步为「待审核」,便于管理员直接审核通过/拒绝。
func (s *CertificationApplicationServiceImpl) syncCertToPendingReviewIfRecordPending(ctx context.Context, cert *entities.Certification, record *entities.EnterpriseInfoSubmitRecord) error {
if record.ManualReviewStatus != "pending" {
// AdminTransitionCertificationStatus 管理端按用户变更认证状态(以状态机为准)
func (s *CertificationApplicationServiceImpl) AdminTransitionCertificationStatus(ctx context.Context, cmd *commands.AdminTransitionCertificationStatusCommand) error {
cert, err := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID)
if err != nil {
return fmt.Errorf("加载认证信息失败: %w", err)
}
record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cmd.UserID)
if err != nil {
return fmt.Errorf("查找企业信息提交记录失败: %w", err)
}
if record == nil {
return fmt.Errorf("未找到该用户的企业信息提交记录")
}
switch cmd.TargetStatus {
case string(enums.StatusInfoSubmitted):
// 审核通过:与 AdminApproveSubmitRecord 一致,推状态并生成企业认证链接
switch cert.Status {
case enums.StatusInfoSubmitted, enums.StatusEnterpriseVerified, enums.StatusContractApplied,
enums.StatusContractSigned, enums.StatusCompleted, enums.StatusContractRejected, enums.StatusContractExpired:
return nil
}
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)
}
if err := cert.ApproveEnterpriseInfoReview(authURL.AuthShortURL, authURL.AuthFlowID, cmd.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("user_id", cmd.UserID), zap.String("admin_id", cmd.AdminID))
return nil
case string(enums.StatusInfoRejected):
// 审核拒绝
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))
}
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)
}
s.logger.Info("管理端变更认证状态为拒绝", zap.String("user_id", cmd.UserID), zap.String("admin_id", cmd.AdminID))
return nil
default:
return fmt.Errorf("不支持的目标状态: %s", cmd.TargetStatus)
}
if cert.Status != enums.StatusPending && cert.Status != enums.StatusInfoRejected {
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,
}
if err := cert.SubmitEnterpriseInfoForReview(enterpriseInfo); 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("user_id", cert.UserID), zap.String("record_id", record.ID))
return nil
}
// ================ 辅助方法 ================
// convertToResponse 转换实体为响应DTO
func (s *CertificationApplicationServiceImpl) convertToResponse(cert *entities.Certification) *responses.CertificationResponse {
response := &responses.CertificationResponse{

View File

@@ -94,6 +94,14 @@ type ForceTransitionStatusCommand struct {
Force bool `json:"force,omitempty"` // 是否强制执行,跳过业务规则验证
}
// AdminTransitionCertificationStatusCommand 管理端变更认证状态(以状态机为准,用于审核通过/拒绝等)
type AdminTransitionCertificationStatusCommand struct {
AdminID string `json:"-"`
UserID string `json:"user_id" validate:"required"`
TargetStatus string `json:"target_status" validate:"required,oneof=info_submitted info_rejected"` // 审核通过 -> info_submitted审核拒绝 -> info_rejected
Remark string `json:"remark"`
}
// SubmitEnterpriseInfoCommand 提交企业信息命令
type SubmitEnterpriseInfoCommand struct {
UserID string `json:"-" comment:"用户唯一标识从JWT token获取不在JSON中暴露"`

View File

@@ -193,11 +193,11 @@ func (q *GetSystemMonitoringQuery) ShouldIncludeMetric(metric string) bool {
return false
}
// AdminListSubmitRecordsQuery 管理端企业信息提交记录列表查询
// AdminListSubmitRecordsQuery 管理端企业信息提交记录列表查询(以状态机 certification_status 为准,不做审核状态筛选)
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空为全部
CertificationStatus string `json:"certification_status" form:"certification_status"` // 按认证状态筛选,如 info_pending_review / info_submitted / info_rejected空为全部
CompanyName string `json:"company_name" form:"company_name"` // 企业名称(模糊搜索)
LegalPersonPhone string `json:"legal_person_phone" form:"legal_person_phone"` // 法人手机号
LegalPersonName string `json:"legal_person_name" form:"legal_person_name"` // 法人姓名(模糊搜索)

View File

@@ -119,9 +119,7 @@ type AdminSubmitRecordItem struct {
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"` // 该用户当前认证状态,用于前端判断是否已完成企业认证并显示「已审核」
CertificationStatus string `json:"certification_status,omitempty"` // 以状态机为准info_pending_review/info_submitted/info_rejected 等
}
// AdminSubmitRecordDetail 管理端提交记录详情(含完整信息与图片 URL
@@ -147,11 +145,7 @@ type AdminSubmitRecordDetail struct {
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"` // 该用户当前认证状态,用于前端显示「已审核」
CertificationStatus string `json:"certification_status,omitempty"` // 以状态机为准
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}