199 lines
8.3 KiB
PHP
199 lines
8.3 KiB
PHP
<?php
|
|
/**
|
|
* Identity verification state transitions.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
namespace AKSRT\Identity;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
final class TransitionService {
|
|
|
|
private const ALLOWED = [
|
|
'created' => ['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<string,mixed> $fields @param array<string,mixed> $event */
|
|
public function transition(array $application, string $next, array $fields = [], array $event = []) {
|
|
return $this->transition_with_policy($application, $next, $fields, $event, false);
|
|
}
|
|
|
|
/** @param array<string,mixed> $fields @param array<string,mixed> $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<string,mixed> $fields @param array<string,mixed> $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<string,mixed> $application
|
|
* @param array<string,mixed> $fields
|
|
* @param array<string,mixed> $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<string,mixed> $application
|
|
* @param array<string,mixed> $fields
|
|
* @param array<string,mixed> $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<string,mixed> $application @param array<string,mixed> $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'),
|
|
]);
|
|
}
|
|
}
|