diff --git a/inc/helpers/aksrt-avatar.php b/inc/helpers/aksrt-avatar.php index 7f58255b..ce89327c 100644 --- a/inc/helpers/aksrt-avatar.php +++ b/inc/helpers/aksrt-avatar.php @@ -281,7 +281,7 @@ if (!function_exists('aksrt_render_admin_avatar_field')) { - ID, 96, ['loading' => 'eager']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + ID, 96, '', $user->display_name, ['loading' => 'eager']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

ID, 96) !== '') : ?> diff --git a/src/Admin/Native/UsersMenusVerify.php b/src/Admin/Native/UsersMenusVerify.php index 05c9a178..22d8ecca 100644 --- a/src/Admin/Native/UsersMenusVerify.php +++ b/src/Admin/Native/UsersMenusVerify.php @@ -195,7 +195,7 @@ trait UsersMenusVerify { $application = (new \AKSRT\Identity\ApplicationRepository())->find_by_uuid($uuid); $result = (!$application || (int) $application['user_id'] !== $user_id || (string) $application['status'] !== $expected) ? new \WP_Error('aksrt_verify_conflict', __('认证状态已变化,请刷新后重试', 'aksrt')) - : \AKSRT\User\AuthApply::review($user_id, $status, $note, get_current_user_id()); + : \AKSRT\User\AuthApply::review($user_id, $uuid, $status, $note, get_current_user_id(), []); $extra = ['status' => $status === 'cancelled' ? 'cancelled' : 'pending']; if (is_wp_error($result)) $extra['aksrt_verify_error'] = $result->get_error_message(); $this->redirect('aksrt_user_auth', is_wp_error($result) ? 'verify_failed' : 'verify_updated', $extra); diff --git a/src/Identity/TransitionService.php b/src/Identity/TransitionService.php index f9f76849..bff1be68 100644 --- a/src/Identity/TransitionService.php +++ b/src/Identity/TransitionService.php @@ -76,6 +76,11 @@ final class TransitionService { return $updated; } + /** @return array */ + public static function allowed_targets(string $status): array { + return self::ALLOWED[sanitize_key($status)] ?? []; + } + /** * Apply an identity transition inside an already-owned database transaction. * The caller must lock the application row before invoking this method. diff --git a/src/User/AuthApply.php b/src/User/AuthApply.php index 59883b3d..53fe586f 100644 --- a/src/User/AuthApply.php +++ b/src/User/AuthApply.php @@ -29,6 +29,9 @@ class AuthApply { add_filter('manage_users_custom_column', [$this, 'render_admin_user_column'], 10, 3); add_action('show_user_profile', [$this, 'render_admin_profile_status']); add_action('edit_user_profile', [$this, 'render_admin_profile_status']); + add_action('personal_options_update', [$this, 'handle_admin_profile_update']); + add_action('edit_user_profile_update', [$this, 'handle_admin_profile_update']); + add_action('admin_notices', [$this, 'render_admin_profile_notice']); } } @@ -243,10 +246,12 @@ class AuthApply { return $history; } - public static function review(int $user_id, string $status, string $note, int $reviewer_id) { + /** @param array $profile_fields */ + public static function review(int $user_id, string $application_uuid, string $status, string $note, int $reviewer_id, array $profile_fields) { $repo = new ApplicationRepository(); - $application = $repo->latest_for_user($user_id); - if (!$application) { + $application_uuid = sanitize_text_field($application_uuid); + $application = wp_is_uuid($application_uuid) ? $repo->find_by_uuid($application_uuid) : null; + if (!$application || (int) $application['user_id'] !== $user_id) { return new \WP_Error('aksrt_verify_missing', __('认证申请不存在', 'aksrt')); } $status = sanitize_key($status); @@ -257,10 +262,19 @@ class AuthApply { if (in_array($status, ['rejected', 'cancelled'], true) && $note === '') { return new \WP_Error('aksrt_verify_note', __('请填写处理原因', 'aksrt')); } - $result = (new TransitionService($repo))->transition($application, $status, [ - 'review_note' => $note, - 'reviewer_id' => $reviewer_id, - ], ['source' => 'admin', 'event_type' => 'manual_review', 'actor_user_id' => $reviewer_id]); + $fields = []; + foreach (['public_name', 'public_desc'] as $key) { + if (array_key_exists($key, $profile_fields)) { + $fields[$key] = (string) $profile_fields[$key]; + } + } + $fields['review_note'] = $note; + $fields['reviewer_id'] = $reviewer_id; + $result = (new TransitionService($repo))->transition($application, $status, $fields, [ + 'source' => 'admin', + 'event_type' => 'manual_review', + 'actor_user_id' => $reviewer_id, + ]); return is_wp_error($result) ? $result : ['status' => $status, 'reviewed_at' => (string) $result['completed_at']]; } @@ -279,14 +293,170 @@ class AuthApply { } public function render_admin_profile_status(\WP_User $user): void { - $status = self::get_status((int) $user->ID); - $info = self::get_auth_info((int) $user->ID); + $user_id = (int) $user->ID; + $snapshot = self::snapshot($user_id); + $application = is_array($snapshot['verified'] ?? null) + ? $snapshot['verified'] + : (is_array($snapshot['latest'] ?? null) ? $snapshot['latest'] : null); + $raw_status = (string) ($application['status'] ?? ''); + $can_manage = $application !== null + && current_user_can('aksrt_review_identity') + && current_user_can('edit_user', $user_id); + $allowed_actions = array_values(array_intersect( + TransitionService::allowed_targets($raw_status), + ['verified', 'rejected', 'cancelled'] + )); + $public_name = (string) ($application['public_name'] ?? ''); + $public_desc = (string) ($application['public_desc'] ?? ''); ?>

- - + + + + + + + + + + + + + + + + + + + + + + + snapshot_for_user($user_id); + $application = is_array($snapshot['verified'] ?? null) + ? $snapshot['verified'] + : (is_array($snapshot['latest'] ?? null) ? $snapshot['latest'] : null); + if ($application === null) { + return; + } + $application_uuid = sanitize_text_field((string) wp_unslash($_POST['aksrt_identity_application_uuid'] ?? '')); + if ($application_uuid === '' || !hash_equals((string) $application['application_uuid'], $application_uuid)) { + $this->set_admin_profile_notice('error', __('身份认证记录已变化,请刷新后重试。', 'aksrt')); + return; + } + + $public_name = wp_html_excerpt(sanitize_text_field((string) wp_unslash($_POST['aksrt_identity_public_name'] ?? '')), 64, ''); + $public_desc = wp_html_excerpt(sanitize_textarea_field((string) wp_unslash($_POST['aksrt_identity_public_desc'] ?? '')), 300, ''); + $action = sanitize_key((string) wp_unslash($_POST['aksrt_identity_action'] ?? '')); + $note = sanitize_textarea_field((string) wp_unslash($_POST['aksrt_identity_review_note'] ?? '')); + $fields = [ + 'public_name' => $public_name, + 'public_desc' => $public_desc, + ]; + + if ($action === '') { + $updated = $repo->update((int) $application['id'], $fields); + $this->set_admin_profile_notice($updated ? 'success' : 'error', $updated ? __('身份认证资料已更新。', 'aksrt') : __('身份认证资料更新失败。', 'aksrt')); + return; + } + + $allowed = TransitionService::allowed_targets((string) $application['status']); + if (!in_array($action, $allowed, true) || !in_array($action, ['verified', 'rejected', 'cancelled'], true)) { + $this->set_admin_profile_notice('error', __('当前认证状态不能执行该操作。', 'aksrt')); + return; + } + if ($action === 'verified' && $public_name === '') { + $this->set_admin_profile_notice('error', __('通过认证时请填写认证身份。', 'aksrt')); + return; + } + if (in_array($action, ['rejected', 'cancelled'], true) && $note === '') { + $this->set_admin_profile_notice('error', __('驳回或取消认证时请填写处理说明。', 'aksrt')); + return; + } + + $result = self::review($user_id, $application_uuid, $action, $note, get_current_user_id(), $fields); + $this->set_admin_profile_notice( + is_wp_error($result) ? 'error' : 'success', + is_wp_error($result) ? $result->get_error_message() : __('身份认证已更新。', 'aksrt') + ); + } + + public function render_admin_profile_notice(): void { + $key = 'aksrt_identity_profile_notice_' . get_current_user_id(); + $notice = get_transient($key); + if (!is_array($notice) || empty($notice['message'])) { + return; + } + delete_transient($key); + $type = ($notice['type'] ?? '') === 'success' ? 'success' : 'error'; + echo '

' . esc_html((string) $notice['message']) . '

'; + } + + private function set_admin_profile_notice(string $type, string $message): void { + set_transient('aksrt_identity_profile_notice_' . get_current_user_id(), [ + 'type' => $type, + 'message' => $message, + ], 60); + } + + private static function admin_status_label(string $status): string { + $labels = [ + 'created' => __('已创建', 'aksrt'), + 'provider_pending' => __('等待核身', 'aksrt'), + 'provider_processing' => __('云端处理中', 'aksrt'), + 'provider_failed' => __('云认证失败', 'aksrt'), + 'provider_unsupported' => __('云端不支持', 'aksrt'), + 'provider_expired' => __('会话已过期', 'aksrt'), + 'manual_pending' => __('人工待审', 'aksrt'), + 'verified' => __('已认证', 'aksrt'), + 'rejected' => __('已驳回', 'aksrt'), + 'cancelled' => __('已取消', 'aksrt'), + ]; + return $labels[$status] ?? $status; + } + + private static function admin_action_label(string $action): string { + $labels = [ + 'verified' => __('通过认证', 'aksrt'), + 'rejected' => __('驳回申请', 'aksrt'), + 'cancelled' => __('取消认证', 'aksrt'), + ]; + return $labels[$action] ?? $action; + } } diff --git a/src/User/BalanceAdmin.php b/src/User/BalanceAdmin.php index c669c31d..f464221a 100644 --- a/src/User/BalanceAdmin.php +++ b/src/User/BalanceAdmin.php @@ -80,7 +80,6 @@ class BalanceAdmin { } $balance_minor = \AKSRT\Ledger\LedgerService::get_instance()->balance((int) $user->ID, \AKSRT\Ledger\AssetType::BALANCE); $can = $this->can_manage(); - $log = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['user_id' => (int) $user->ID, 'asset' => 'balance', 'page' => 1, 'per_page' => 8]); ?>

@@ -105,28 +104,6 @@ class BalanceAdmin { - - - - - - - - - - - - - - - - -
0 ? '+' : '-') . \AKSRT\Ledger\AssetAmount::from_minor(\AKSRT\Ledger\AssetType::BALANCE, abs((int) $row['delta_minor']))->formatted()); ?>formatted()); ?>
- -

- - - - history(['user_id' => (int) $user->ID, 'asset' => 'points', 'page' => 1, 'per_page' => 20]); - $items = $log['items'] ?? []; - ?> -

- - - - - - - - - - - - - - - - - - - - - - -
- 0 ? '+' : '') . $delta); ?> -
- -

- render_badge($level); return '' . '' . $icon . '' . esc_html(Level::get_level_name($level)) . '' @@ -117,7 +117,7 @@ class LevelAdmin { - ID, 'aksrt-user-level-icon--admin'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + render_badge((int) $progress['level']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

@@ -156,4 +156,13 @@ class LevelAdmin { update_user_meta($user_id, Level::META_INTEGRAL, $integral); update_user_meta($user_id, Level::META_LEVEL, $level); } + + private function render_badge(int $level): string { + $name = Level::get_level_name($level); + $icon = Level::get_level_icon_url($level); + if ($icon !== '') { + return '' . esc_attr($name) . ''; + } + return 'LV' . esc_html((string) $level) . ''; + } } diff --git a/tests/widget-system/user-features-contract.test.cjs b/tests/widget-system/user-features-contract.test.cjs index 37d922e0..5163aac7 100644 --- a/tests/widget-system/user-features-contract.test.cjs +++ b/tests/widget-system/user-features-contract.test.cjs @@ -167,3 +167,36 @@ test('retired admin pages, schemas, APIs, and metadata chains are absent', () => assert.match(read('assets/admin/src/api/modules/tools.js'), /\/settings\/health/); assert.match(read('assets/admin/src/constants/sections/tools.js'), /route: '\/tools\/health'/); }); + +test('native user profile omits ledger tables and renders canonical images eagerly', () => { + const credits = read('src/User/CreditsAdmin.php'); + const balance = read('src/User/BalanceAdmin.php'); + const avatar = read('inc/helpers/aksrt-avatar.php'); + const level = read('src/User/LevelAdmin.php'); + + assert.doesNotMatch(credits, /LedgerQuery::get_instance\(\)->history/); + assert.doesNotMatch(credits, /近期%s流水|近期积分流水/); + assert.doesNotMatch(balance, /LedgerQuery::get_instance\(\)->history/); + assert.doesNotMatch(balance, /近期流水/); + assert.match(avatar, /get_avatar\(\$user->ID, 96, '', \$user->display_name, \['loading' => 'eager'\]\)/); + assert.doesNotMatch(avatar, /aksrt_get_avatar_html\(\$user->ID, 96/); + assert.match(level, /Level::get_level_icon_url\(\$level\)/); + assert.match(level, /