This commit is contained in:
@@ -49,11 +49,12 @@ type ApplicationSubmitResult struct {
|
||||
InstanceStatus int
|
||||
}
|
||||
|
||||
// ApplicationService 创建与重提核销申请。
|
||||
// ApplicationService 创建与重提核销申请,并恢复原审批提交事实。
|
||||
// 申请、审批尝试记录、审批实例与账单预占在同一事务完成;任一校验失败都不留下半成品事实。
|
||||
type ApplicationService struct {
|
||||
db *gorm.DB
|
||||
approval approvalapp.Port
|
||||
recovery approvalapp.RecoveryPort
|
||||
audit ApplicationAuditWriter
|
||||
}
|
||||
|
||||
@@ -62,6 +63,22 @@ func NewApplicationService(db *gorm.DB, approval approvalapp.Port, audit Applica
|
||||
return &ApplicationService{db: db, approval: approval, audit: audit}
|
||||
}
|
||||
|
||||
// SetRecoveryPort 注入通用审批原实例恢复接缝。
|
||||
func (s *ApplicationService) SetRecoveryPort(recovery approvalapp.RecoveryPort) {
|
||||
s.recovery = recovery
|
||||
}
|
||||
|
||||
// RecoveryResult 返回原审批恢复动作的安全结果。
|
||||
type RecoveryResult struct {
|
||||
ApplicationID uint
|
||||
ApprovalInstanceID uint
|
||||
Action string
|
||||
Result string
|
||||
SubmissionStatus int
|
||||
LastRecoveryAt *time.Time
|
||||
Recoverable bool
|
||||
}
|
||||
|
||||
// Create 为本人可见账单创建核销申请;超级管理员可为账单欠款人代办并必须填写代办原因。
|
||||
func (s *ApplicationService) Create(ctx context.Context, command SubmitApplicationCommand) (*ApplicationSubmitResult, error) {
|
||||
if err := s.ensureReady(); err != nil {
|
||||
@@ -89,6 +106,81 @@ func (s *ApplicationService) Resubmit(ctx context.Context, applicationID uint, c
|
||||
return s.submit(ctx, caller, applicationID, command)
|
||||
}
|
||||
|
||||
// RecoverApproval 恢复员工核销申请的原审批事实,不创建新尝试或审批实例。
|
||||
func (s *ApplicationService) RecoverApproval(ctx context.Context, applicationID uint) (*RecoveryResult, error) {
|
||||
if applicationID == 0 {
|
||||
return nil, errors.New(errors.CodeEmployeeCollectionApplicationNotFound)
|
||||
}
|
||||
actorID := middleware.GetUserIDFromContext(ctx)
|
||||
if actorID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
if userType == constants.UserTypeAgent || userType == constants.UserTypeEnterprise {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
if s == nil || s.db == nil || s.recovery == nil || s.audit == nil {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "核销审批恢复能力尚未配置")
|
||||
}
|
||||
var result RecoveryResult
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
application, err := lockApplication(ctx, tx, applicationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if application.ApplicantAccountID != actorID && userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {
|
||||
return errors.New(errors.CodeEmployeeCollectionApplicationNotFound)
|
||||
}
|
||||
if application.Status != constants.EmployeeCollectionApplicationStatusPending || application.LatestAttemptID == 0 || application.LatestApprovalInstanceID == 0 {
|
||||
return errors.New(errors.CodeConflict, "终态申请不可恢复")
|
||||
}
|
||||
var attempt model.EmployeeCollectionApplicationAttempt
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND application_id = ?", application.LatestAttemptID, application.ID).First(&attempt).Error; err != nil {
|
||||
return errors.New(errors.CodeConflict, "核销审批尝试关联不一致")
|
||||
}
|
||||
if attempt.ApprovalInstanceID == nil || *attempt.ApprovalInstanceID != application.LatestApprovalInstanceID {
|
||||
return errors.New(errors.CodeConflict, "核销审批实例关联不一致")
|
||||
}
|
||||
var instance model.ApprovalInstance
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND business_type = ? AND business_id = ?", application.LatestApprovalInstanceID, constants.ApprovalBusinessTypeEmployeeCollection, attempt.ID).First(&instance).Error; err != nil {
|
||||
return errors.New(errors.CodeConflict, "审批实例业务关联不一致")
|
||||
}
|
||||
if instance.Status != constants.ApprovalStatusSubmitting && instance.Status != constants.ApprovalStatusPending && instance.Status != constants.ApprovalStatusSubmissionFailed && instance.Status != constants.ApprovalStatusSubmissionUnknown {
|
||||
return errors.New(errors.CodeConflict, "审批终态不允许恢复")
|
||||
}
|
||||
var wc model.WeComApprovalContext
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("approval_instance_id = ? AND business_type = ?", instance.ID, constants.ApprovalBusinessTypeEmployeeCollection).First(&wc).Error; err != nil {
|
||||
return errors.New(errors.CodeConflict, "企业微信审批上下文不存在")
|
||||
}
|
||||
beforeStatus := fmt.Sprintf("%d", wc.SubmissionStatus)
|
||||
action, recoveryErr := "active", error(nil)
|
||||
if wc.SPNo != "" {
|
||||
action = "sp_no"
|
||||
recoveryErr = s.recovery.EnqueueSubmittedSync(ctx, tx, instance.ID)
|
||||
} else if wc.SubmissionStatus == constants.WeComSubmissionStatusUnknown || instance.Status == constants.ApprovalStatusSubmissionUnknown {
|
||||
action = "unknown"
|
||||
recoveryErr = s.recovery.EnqueueUnknownConfirm(ctx, tx, instance.ID)
|
||||
} else if instance.Status == constants.ApprovalStatusSubmissionFailed && wc.SubmissionStatus == constants.WeComSubmissionStatusFailed {
|
||||
action = "replay"
|
||||
_, recoveryErr = s.recovery.RecoverSubmissionEvent(ctx, tx, instance.ID)
|
||||
} else if instance.Status != constants.ApprovalStatusSubmitting && instance.Status != constants.ApprovalStatusPending && wc.SubmissionStatus != constants.WeComSubmissionStatusReady && wc.SubmissionStatus != constants.WeComSubmissionStatusSending {
|
||||
return errors.New(errors.CodeConflict, "当前审批提交状态不允许恢复")
|
||||
}
|
||||
if recoveryErr != nil {
|
||||
return errors.Wrap(errors.CodeConflict, recoveryErr, "恢复审批提交失败")
|
||||
}
|
||||
if err := s.audit.WriteEmployeeCollectionRecovery(ctx, tx, application, &attempt, &instance, actorID, beforeStatus, action, "accepted"); err != nil {
|
||||
return err
|
||||
}
|
||||
result = RecoveryResult{ApplicationID: application.ID, ApprovalInstanceID: instance.ID, Action: action, Result: "accepted", SubmissionStatus: wc.SubmissionStatus, LastRecoveryAt: wc.LastRecoveryAt, Recoverable: true}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ensureReady 校验用例依赖完整,缺失时失败关闭,避免绕过企业微信终审。
|
||||
func (s *ApplicationService) ensureReady() error {
|
||||
if s == nil || s.db == nil || s.approval == nil || s.audit == nil {
|
||||
|
||||
Reference in New Issue
Block a user