perf(modal): unify cancellable frontend modal flows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 02:04:55 +08:00
co-authored by Claude Fable 5
parent 25ce1d8f43
commit 5375ad6429
36 changed files with 2623 additions and 1806 deletions
+87 -21
View File
@@ -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> */
-1
View File
@@ -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'],
+53 -10
View File
@@ -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;
}
}