['provider_pending', 'provider_failed', 'provider_unsupported', 'provider_expired', 'manual_pending', 'cancelled'], 'provider_pending' => ['provider_processing', 'provider_failed', 'provider_unsupported', 'provider_expired', 'verified', 'manual_pending', 'cancelled'], 'provider_processing' => ['provider_failed', 'provider_unsupported', 'provider_expired', 'verified', 'manual_pending'], 'provider_failed' => ['provider_pending', 'manual_pending', 'rejected', 'cancelled'], 'provider_unsupported' => ['manual_pending', 'rejected', 'cancelled'], 'provider_expired' => ['provider_pending', 'manual_pending', 'rejected', 'cancelled'], 'manual_pending' => ['verified', 'rejected', 'cancelled'], 'verified' => ['cancelled'], ]; public function __construct( private ?ApplicationRepository $applications = null, private ?EventRepository $events = null ) { $this->applications ??= new ApplicationRepository(); $this->events ??= new EventRepository(); } /** @param array $fields @param array $event */ public function transition(array $application, string $next, array $fields = [], array $event = []) { return $this->transition_with_policy($application, $next, $fields, $event, false); } /** @param array $fields @param array $event */ public function admin_transition(array $application, string $next, array $fields = [], array $event = []) { return $this->transition_with_policy($application, $next, $fields, $event, true); } /** @param array $fields @param array $event */ private function transition_with_policy(array $application, string $next, array $fields, array $event, bool $admin_override) { $next = sanitize_key($next); $dedupe = isset($event['dedupe_key']) ? (string) $event['dedupe_key'] : ''; if ($dedupe !== '') { $existing_event = $this->events->find_by_dedupe($dedupe); if (is_array($existing_event) && (int) $existing_event['application_id'] === (int) $application['id']) { return $this->applications->find_by_id((int) $application['id']); } } if (\AKSRT\Ledger\LedgerService::get_instance()->transaction_active()) { return new \WP_Error('aksrt_identity_nested_transaction', __('认证状态不能开启嵌套事务', 'aksrt'), ['status' => 500]); } global $wpdb; if ($wpdb->query('START TRANSACTION') === false) { return new \WP_Error('aksrt_identity_db', __('认证状态更新失败', 'aksrt'), ['status' => 500]); } $locked = $this->applications->find_by_id_for_update((int) $application['id']); if (!is_array($locked)) { $wpdb->query('ROLLBACK'); return new \WP_Error('aksrt_identity_missing', __('认证申请不存在', 'aksrt'), ['status' => 404]); } if ($dedupe !== '') { $existing_event = $this->events->find_by_dedupe($dedupe); if (is_array($existing_event) && (int) $existing_event['application_id'] === (int) $locked['id']) { $wpdb->query('COMMIT'); return $locked; } } $changed = $this->apply_transition_locked($locked, $next, $fields, $event, $admin_override); if (is_wp_error($changed)) { $wpdb->query('ROLLBACK'); return $changed; } $updated = $this->applications->find_by_id((int) $locked['id']); if (!is_array($updated) || $wpdb->query('COMMIT') === false) { $wpdb->query('ROLLBACK'); return new \WP_Error('aksrt_identity_db', __('认证状态更新失败', 'aksrt'), ['status' => 500]); } $this->after_commit($locked, $next, $fields); return $updated; } /** * Apply an identity transition inside an already-owned database transaction. * The caller must lock the application row before invoking this method. * * @param array $application * @param array $fields * @param array $event * @return true|\WP_Error */ public function transition_locked(array $application, string $next, array $fields = [], array $event = []) { return $this->apply_transition_locked($application, $next, $fields, $event, false); } /** * @param array $application * @param array $fields * @param array $event * @return true|\WP_Error */ private function apply_transition_locked(array $application, string $next, array $fields, array $event, bool $admin_override) { $current = sanitize_key((string) ($application['status'] ?? '')); $next = sanitize_key($next); $allowed = $admin_override ? in_array($next, ['manual_pending', 'verified', 'rejected', 'cancelled'], true) && $next !== $current : in_array($next, self::ALLOWED[$current] ?? [], true); if (!$allowed) { return new \WP_Error('aksrt_identity_transition', __('该认证记录当前不可执行此操作', 'aksrt'), ['status' => 409]); } if (in_array($next, ['verified', 'rejected', 'cancelled'], true)) { $fields['completed_at'] = current_time('mysql', true); $fields['sensitive_blob'] = null; } else { $fields['completed_at'] = null; } if (!$this->applications->transition((int) $application['id'], $current, $next, $fields)) { return new \WP_Error('aksrt_identity_conflict', __('认证状态已变化,请刷新后重试', 'aksrt'), ['status' => 409]); } $dedupe = (string) ($event['dedupe_key'] ?? hash('sha256', implode('|', [ (string) $application['application_uuid'], $current, $next, wp_generate_uuid4(), ]))); $recorded = $this->events->record([ 'application_id' => (int) $application['id'], 'source' => (string) ($event['source'] ?? 'system'), 'event_type' => (string) ($event['event_type'] ?? 'status_changed'), 'external_event_id' => (string) ($event['external_event_id'] ?? ''), 'dedupe_key' => $dedupe, 'from_status' => $current, 'to_status' => $next, 'result_code' => (string) ($event['result_code'] ?? ''), 'summary' => is_array($event['summary'] ?? null) ? $event['summary'] : [], 'actor_user_id' => (int) ($event['actor_user_id'] ?? 0), ]); return $recorded ? true : new \WP_Error('aksrt_identity_event', __('认证事件写入失败', 'aksrt'), ['status' => 500]); } /** @param array $application @param array $fields */ public function after_commit(array $application, string $next, array $fields = []): void { try { if ($next === 'verified') { $this->send_notification((int) $application['user_id'], 'verified', ''); } elseif ($next === 'rejected' || $next === 'cancelled') { $this->send_notification((int) $application['user_id'], $next, (string) ($fields['review_note'] ?? '')); } do_action('aksrt_user_auth_reviewed', (int) $application['user_id'], self::public_status($next), (string) ($fields['review_note'] ?? ''), (int) ($fields['reviewer_id'] ?? 0)); } catch (\Throwable $error) { if (defined('WP_DEBUG') && WP_DEBUG) { error_log('[AKSRT Identity Event] ' . $error->getMessage()); } } } public static function public_status(string $status): string { if ($status === 'verified') { return 'verified'; } if ($status === 'cancelled') { return 'cancelled'; } if ($status === 'rejected') { return 'rejected'; } return in_array($status, ApplicationRepository::ACTIVE_STATUSES, true) ? 'pending' : 'rejected'; } private function send_notification(int $user_id, string $status, string $note): void { if (!class_exists('\\AKSRT\\Messages\\Message')) { return; } if ($status === 'verified') { $title = __('官方认证结果', 'aksrt'); $message = __('您的官方认证已通过', 'aksrt'); } elseif ($status === 'cancelled') { $title = __('官方认证已取消', 'aksrt'); $message = sprintf(__('您的官方认证已被取消:%s', 'aksrt'), $note); } else { $title = __('官方认证结果', 'aksrt'); $message = sprintf(__('您的官方认证申请未通过:%s', 'aksrt'), $note); } \AKSRT\Messages\Message::get_instance()->send($user_id, 'auth_apply_reply', $message, [ 'title' => $title, 'target_url' => aksrt_get_user_center_tab_url('auth'), ]); } }