perf(modal): unify cancellable frontend modal flows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,10 +17,27 @@ final class ApplicationRepository {
|
||||
|
||||
private string $table;
|
||||
|
||||
/** @var array<int,array{latest:array<string,mixed>|null,active:array<string,mixed>|null,verified:array<string,mixed>|null}> */
|
||||
private static array $snapshot_cache = [];
|
||||
|
||||
public function __construct() {
|
||||
$this->table = \AKSRT\Core\DBMigrator::table('identity_application');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the request-local application snapshot cache.
|
||||
*
|
||||
* Writes can change the active/latest/verified selection, so callers must
|
||||
* invalidate before reading the next view in the same request.
|
||||
*/
|
||||
public static function clear_snapshot_cache(int $user_id = 0): void {
|
||||
if ($user_id > 0) {
|
||||
unset(self::$snapshot_cache[$user_id]);
|
||||
return;
|
||||
}
|
||||
self::$snapshot_cache = [];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $data */
|
||||
public function create(array $data) {
|
||||
global $wpdb;
|
||||
@@ -47,6 +64,7 @@ final class ApplicationRepository {
|
||||
if ($inserted !== 1) {
|
||||
return new \WP_Error('aksrt_identity_create', __('认证申请创建失败', 'aksrt'));
|
||||
}
|
||||
self::clear_snapshot_cache((int) $row['user_id']);
|
||||
return $this->find_by_id((int) $wpdb->insert_id);
|
||||
}
|
||||
|
||||
@@ -75,36 +93,63 @@ final class ApplicationRepository {
|
||||
return is_array($row) ? $this->normalize($row) : null;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
public function latest_for_user(int $user_id): ?array {
|
||||
/**
|
||||
* Load the latest, active, and verified applications in one request-local
|
||||
* context. The three views are selected from one ordered result set so
|
||||
* facade callers cannot repeat the same application SELECT.
|
||||
*
|
||||
* @return array{latest:array<string,mixed>|null,active:array<string,mixed>|null,verified:array<string,mixed>|null}
|
||||
*/
|
||||
public function snapshot_for_user(int $user_id): array {
|
||||
if ($user_id <= 0) {
|
||||
return ['latest' => null, 'active' => null, 'verified' => null];
|
||||
}
|
||||
if (array_key_exists($user_id, self::$snapshot_cache)) {
|
||||
return self::$snapshot_cache[$user_id];
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table} WHERE user_id = %d ORDER BY id DESC LIMIT 1",
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table} WHERE user_id = %d ORDER BY id DESC",
|
||||
$user_id
|
||||
), ARRAY_A);
|
||||
return is_array($row) ? $this->normalize($row) : null;
|
||||
$snapshot = ['latest' => null, 'active' => null, 'verified' => null];
|
||||
foreach ((array) $rows as $raw) {
|
||||
if (!is_array($raw)) {
|
||||
continue;
|
||||
}
|
||||
$record = $this->normalize($raw);
|
||||
if ($snapshot['latest'] === null) {
|
||||
$snapshot['latest'] = $record;
|
||||
}
|
||||
if ($snapshot['active'] === null && in_array((string) $record['status'], self::ACTIVE_STATUSES, true)) {
|
||||
$snapshot['active'] = $record;
|
||||
}
|
||||
if ((string) $record['status'] === 'verified') {
|
||||
$current = $snapshot['verified'];
|
||||
if ($current === null || $this->verified_is_newer($record, $current)) {
|
||||
$snapshot['verified'] = $record;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self::$snapshot_cache[$user_id] = $snapshot;
|
||||
return $snapshot;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
public function latest_for_user(int $user_id): ?array {
|
||||
return $this->snapshot_for_user($user_id)['latest'];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
public function active_for_user(int $user_id): ?array {
|
||||
global $wpdb;
|
||||
$placeholders = implode(',', array_fill(0, count(self::ACTIVE_STATUSES), '%s'));
|
||||
$params = array_merge([$user_id], self::ACTIVE_STATUSES);
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table} WHERE user_id = %d AND status IN ({$placeholders}) ORDER BY id DESC LIMIT 1",
|
||||
$params
|
||||
), ARRAY_A);
|
||||
return is_array($row) ? $this->normalize($row) : null;
|
||||
return $this->snapshot_for_user($user_id)['active'];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
public function verified_for_user(int $user_id): ?array {
|
||||
global $wpdb;
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table} WHERE user_id = %d AND status = 'verified' ORDER BY completed_at DESC, id DESC LIMIT 1",
|
||||
$user_id
|
||||
), ARRAY_A);
|
||||
return is_array($row) ? $this->normalize($row) : null;
|
||||
return $this->snapshot_for_user($user_id)['verified'];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $fields */
|
||||
@@ -125,7 +170,11 @@ final class ApplicationRepository {
|
||||
$formats[$index] = '%f';
|
||||
}
|
||||
}
|
||||
return $wpdb->update($this->table, $fields, ['id' => $id, 'status' => $expected], $formats, ['%d', '%s']) === 1;
|
||||
$updated = $wpdb->update($this->table, $fields, ['id' => $id, 'status' => $expected], $formats, ['%d', '%s']) === 1;
|
||||
if ($updated) {
|
||||
self::clear_snapshot_cache();
|
||||
}
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $fields */
|
||||
@@ -136,6 +185,9 @@ final class ApplicationRepository {
|
||||
}
|
||||
$fields['updated_at'] = current_time('mysql', true);
|
||||
$updated = $wpdb->update($this->table, $fields, ['id' => $id]);
|
||||
if ($updated !== false) {
|
||||
self::clear_snapshot_cache();
|
||||
}
|
||||
return $updated !== false;
|
||||
}
|
||||
|
||||
@@ -150,7 +202,21 @@ final class ApplicationRepository {
|
||||
|
||||
public function delete(int $id): bool {
|
||||
global $wpdb;
|
||||
return $wpdb->delete($this->table, ['id' => $id], ['%d']) === 1;
|
||||
$deleted = $wpdb->delete($this->table, ['id' => $id], ['%d']) === 1;
|
||||
if ($deleted) {
|
||||
self::clear_snapshot_cache();
|
||||
}
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $record @param array<string,mixed> $current */
|
||||
private function verified_is_newer(array $record, array $current): bool {
|
||||
$record_time = strtotime((string) ($record['completed_at'] ?? '')) ?: 0;
|
||||
$current_time = strtotime((string) ($current['completed_at'] ?? '')) ?: 0;
|
||||
if ($record_time !== $current_time) {
|
||||
return $record_time > $current_time;
|
||||
}
|
||||
return (int) ($record['id'] ?? 0) > (int) ($current['id'] ?? 0);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
|
||||
@@ -189,7 +189,6 @@ final class ApplicationService {
|
||||
'result_code' => (string) $application['provider_result_code'],
|
||||
'message' => (string) $application['provider_result_summary'],
|
||||
'redirect_url' => esc_url_raw((string) ($application['redirect_url'] ?? '')),
|
||||
'qr_payload' => sanitize_text_field((string) ($application['qr_payload'] ?? '')),
|
||||
'expires_at' => (string) ($application['expires_at'] ?? ''),
|
||||
'created_at' => (string) $application['created_at'],
|
||||
'poll_interval' => (int) SettingsRuntime::get()['poll_interval_seconds'],
|
||||
|
||||
@@ -13,6 +13,9 @@ final class RestController {
|
||||
|
||||
private const NS = 'aksrt/v1';
|
||||
|
||||
private ?array $request_application = null;
|
||||
private string $request_uuid = '';
|
||||
|
||||
public function __construct(
|
||||
private ?ApplicationRepository $applications = null,
|
||||
private ?ApplicationService $service = null
|
||||
@@ -57,7 +60,7 @@ final class RestController {
|
||||
if (!is_user_logged_in()) {
|
||||
return false;
|
||||
}
|
||||
$application = $this->applications->find_by_uuid((string) $request['uuid']);
|
||||
$application = $this->application_for_request($request);
|
||||
return is_array($application) && (int) $application['user_id'] === get_current_user_id();
|
||||
}
|
||||
|
||||
@@ -69,34 +72,74 @@ final class RestController {
|
||||
}
|
||||
|
||||
public function current(): \WP_REST_Response {
|
||||
$application = $this->applications->latest_for_user(get_current_user_id());
|
||||
return new \WP_REST_Response(['success' => true, 'data' => $application ? $this->service->view($application) : null]);
|
||||
$user_id = get_current_user_id();
|
||||
$data = class_exists('\\AKSRT\\User\\AuthApply')
|
||||
? \AKSRT\User\AuthApply::snapshot_view($user_id)
|
||||
: ['user_id' => $user_id, 'current' => null, 'active' => null, 'latest' => null];
|
||||
return $this->no_store(new \WP_REST_Response(['success' => true, 'data' => $data]));
|
||||
}
|
||||
|
||||
public function get(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$application = $this->applications->find_by_uuid((string) $request['uuid']);
|
||||
return new \WP_REST_Response(['success' => true, 'data' => $this->service->view($application)]);
|
||||
$application = $this->application_for_request($request);
|
||||
if (!$application) {
|
||||
return $this->no_store(new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'aksrt_identity_missing',
|
||||
'message' => __('认证申请不存在', 'aksrt'),
|
||||
], 404));
|
||||
}
|
||||
return $this->no_store(new \WP_REST_Response(['success' => true, 'data' => $this->service->view($application)]));
|
||||
}
|
||||
|
||||
public function refresh(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$application = $this->applications->find_by_uuid((string) $request['uuid']);
|
||||
$application = $this->application_for_request($request);
|
||||
if (!$application) {
|
||||
return $this->no_store(new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'aksrt_identity_missing',
|
||||
'message' => __('认证申请不存在', 'aksrt'),
|
||||
], 404));
|
||||
}
|
||||
return $this->respond($this->service->refresh($application));
|
||||
}
|
||||
|
||||
public function manual(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$application = $this->applications->find_by_uuid((string) $request['uuid']);
|
||||
$application = $this->application_for_request($request);
|
||||
if (!$application) {
|
||||
return $this->no_store(new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'aksrt_identity_missing',
|
||||
'message' => __('认证申请不存在', 'aksrt'),
|
||||
], 404));
|
||||
}
|
||||
return $this->respond($this->service->move_to_manual($application, $_FILES['evidence'] ?? null));
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
private function application_for_request(\WP_REST_Request $request): ?array {
|
||||
$uuid = (string) $request['uuid'];
|
||||
if ($uuid !== $this->request_uuid) {
|
||||
$this->request_uuid = $uuid;
|
||||
$this->request_application = $this->applications->find_by_uuid($uuid);
|
||||
}
|
||||
return is_array($this->request_application) ? $this->request_application : null;
|
||||
}
|
||||
|
||||
private function respond($result, int $success_status = 200): \WP_REST_Response {
|
||||
if (is_wp_error($result)) {
|
||||
$status = in_array($result->get_error_code(), ['aksrt_identity_active', 'aksrt_identity_verified', 'aksrt_identity_conflict'], true) ? 409 : 422;
|
||||
return new \WP_REST_Response([
|
||||
return $this->no_store(new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => $result->get_error_code(),
|
||||
'message' => $result->get_error_message(),
|
||||
], $status);
|
||||
], $status));
|
||||
}
|
||||
return new \WP_REST_Response(['success' => true, 'data' => $result], $success_status);
|
||||
return $this->no_store(new \WP_REST_Response(['success' => true, 'data' => $result], $success_status));
|
||||
}
|
||||
|
||||
private function no_store(\WP_REST_Response $response): \WP_REST_Response {
|
||||
$response->header('Cache-Control', 'no-store, private');
|
||||
$response->header('Pragma', 'no-cache');
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user