feat(identity): align auth flow with reference theme
This commit is contained in:
1 parent
5a17b399a0
commit
59901b9e10
34 files changed
+753
-3871
No files matched your search
+161
-477
@@ -1,510 +1,194 @@
|
||||
<?php
|
||||
/**
|
||||
* Identity verification facade for existing presentation call sites.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
/** General user identity certification matching the reference theme flow. @package AKSRT */
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
use AKSRT\Identity\ApplicationRepository;
|
||||
use AKSRT\Identity\EvidenceService;
|
||||
use AKSRT\Identity\EventRepository;
|
||||
use AKSRT\Identity\TransitionService;
|
||||
use AKSRT\Identity\SettingsRuntime;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AuthApply {
|
||||
final class AuthApply {
|
||||
|
||||
private static ?self $instance = null;
|
||||
private static ?self $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
return self::$instance ??= new self();
|
||||
}
|
||||
public static function get_instance(): self {
|
||||
return self::$instance ??= new self();
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (is_admin()) {
|
||||
add_filter('manage_users_columns', [$this, 'add_admin_user_column']);
|
||||
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']);
|
||||
}
|
||||
}
|
||||
public function init(): void {
|
||||
if (!is_admin()) return;
|
||||
add_action('show_user_profile', [$this, 'render_admin_profile']);
|
||||
add_action('edit_user_profile', [$this, 'render_admin_profile']);
|
||||
add_action('personal_options_update', [$this, 'save_admin_profile']);
|
||||
add_action('edit_user_profile_update', [$this, 'save_admin_profile']);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public static function settings(): array {
|
||||
$identity = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_user_identity_settings() : [];
|
||||
return is_array($identity['verify'] ?? null) ? $identity['verify'] : [];
|
||||
}
|
||||
/** @return array<string,mixed> */
|
||||
public static function settings(): array {
|
||||
$all = class_exists('AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_user_identity_settings() : [];
|
||||
return is_array($all['verify'] ?? null) ? $all['verify'] : [];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public static function type_settings(string $type): array {
|
||||
$types = self::settings()['types'] ?? [];
|
||||
$type = sanitize_key($type);
|
||||
return is_array($types[$type] ?? null) ? $types[$type] : [];
|
||||
}
|
||||
/** @return array<string,mixed> */
|
||||
public static function snapshot(int $user_id): array {
|
||||
$repo = new ApplicationRepository();
|
||||
$active = $user_id > 0 ? $repo->active_for_user($user_id) : null;
|
||||
$verified = self::is_verified($user_id);
|
||||
$settings = self::settings();
|
||||
$minimum_level = max(0, (int) ($settings['minimum_level'] ?? 0));
|
||||
$level_settings = class_exists('AKSRT\\Settings\\Settings')
|
||||
? \AKSRT\Settings\Settings::get_user_identity_settings()
|
||||
: [];
|
||||
$level_enabled = !isset($level_settings['level']['enabled']) || !empty($level_settings['level']['enabled']);
|
||||
$level_ok = self::meets_minimum_level($user_id, $minimum_level, $level_enabled);
|
||||
return [
|
||||
'user_id' => $user_id,
|
||||
'verified' => $verified,
|
||||
'status' => $verified ? 'verified' : ($active ? TransitionService::public_status((string) ($active['status'] ?? '')) : ''),
|
||||
'can_apply' => !empty($settings['enabled']) && !empty($settings['allow_apply']) && !$verified && !$active && $level_ok,
|
||||
'level_ok' => $level_ok,
|
||||
'minimum_level' => $minimum_level,
|
||||
'application' => $active,
|
||||
'info' => self::get_auth_info($user_id),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current user's identity state from one repository context.
|
||||
*
|
||||
* The repository memoizes the latest/active/verified records for the
|
||||
* request, while this typed facade keeps all presentation callers on the
|
||||
* same application selection and next-action contract.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function snapshot(int $user_id): array {
|
||||
$user_id = absint($user_id);
|
||||
$settings = self::settings();
|
||||
$records = (new ApplicationRepository())->snapshot_for_user($user_id);
|
||||
$verified = is_array($records['verified'] ?? null) ? $records['verified'] : null;
|
||||
$latest = is_array($records['latest'] ?? null) ? $records['latest'] : null;
|
||||
$active = is_array($records['active'] ?? null) ? $records['active'] : null;
|
||||
$status_record = $verified ?: $latest;
|
||||
$status = $status_record
|
||||
? TransitionService::public_status((string) ($status_record['status'] ?? ''))
|
||||
: '';
|
||||
$current = $active ?: $latest;
|
||||
$stage = $active
|
||||
? (string) ($active['status'] ?? '')
|
||||
: (string) ($latest['status'] ?? '');
|
||||
$next_action = self::next_action($stage);
|
||||
$runtime_settings = class_exists('\\AKSRT\\Settings\\Settings') ? SettingsRuntime::get() : [];
|
||||
$poll_interval = max(5, (int) ($runtime_settings['poll_interval_seconds'] ?? 5));
|
||||
$can_apply = !empty($settings['enabled']) && !empty($settings['allow_apply']) && $status !== 'verified';
|
||||
public static function is_verified(int $user_id): bool {
|
||||
return $user_id > 0 && (bool) get_user_meta($user_id, 'auth', true);
|
||||
}
|
||||
|
||||
return [
|
||||
'user_id' => $user_id,
|
||||
'verified' => $verified !== null,
|
||||
'status' => $status,
|
||||
'stage' => $stage,
|
||||
'next_action' => $next_action,
|
||||
'poll_interval' => $poll_interval,
|
||||
'can_apply' => $can_apply,
|
||||
'latest' => $latest,
|
||||
'active' => $active,
|
||||
'current' => $current,
|
||||
'info' => $verified ? self::info_from_application($verified) : [],
|
||||
'apply' => self::payload_from_application($latest),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe browser-facing snapshot; raw application rows never cross this
|
||||
* boundary.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function snapshot_view(int $user_id): array {
|
||||
$snapshot = self::snapshot($user_id);
|
||||
$latest = is_array($snapshot['latest'] ?? null) ? $snapshot['latest'] : null;
|
||||
$active = is_array($snapshot['active'] ?? null) ? $snapshot['active'] : null;
|
||||
$current = is_array($snapshot['current'] ?? null) ? $snapshot['current'] : null;
|
||||
|
||||
return [
|
||||
'user_id' => (int) ($snapshot['user_id'] ?? 0),
|
||||
'verified' => !empty($snapshot['verified']),
|
||||
'status' => (string) ($snapshot['status'] ?? ''),
|
||||
'stage' => (string) ($snapshot['stage'] ?? ''),
|
||||
'next_action' => (string) ($snapshot['next_action'] ?? 'wait'),
|
||||
'poll_interval' => (int) ($snapshot['poll_interval'] ?? 5),
|
||||
'can_apply' => !empty($snapshot['can_apply']),
|
||||
'info' => is_array($snapshot['info'] ?? null) ? $snapshot['info'] : [],
|
||||
'current' => $current ? self::application_view_model($current, (int) ($snapshot['poll_interval'] ?? 5)) : null,
|
||||
'active' => $active ? self::application_view_model($active, (int) ($snapshot['poll_interval'] ?? 5)) : null,
|
||||
'latest' => $latest ? self::application_view_model($latest, (int) ($snapshot['poll_interval'] ?? 5)) : null,
|
||||
];
|
||||
}
|
||||
|
||||
private static function next_action(string $stage): string {
|
||||
if (in_array($stage, ['provider_pending', 'provider_processing'], true)) {
|
||||
return 'poll';
|
||||
}
|
||||
if (in_array($stage, ['provider_failed', 'provider_unsupported', 'provider_expired'], true)) {
|
||||
return 'manual_or_retry';
|
||||
}
|
||||
if ($stage === 'verified') {
|
||||
return 'complete';
|
||||
}
|
||||
return 'wait';
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function info_from_application(array $application): array {
|
||||
return [
|
||||
'name' => (string) ($application['public_name'] ?? ''),
|
||||
'type' => (string) ($application['type'] ?? ''),
|
||||
'time' => (string) (($application['completed_at'] ?? '') ?: ($application['updated_at'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function payload_from_application(?array $application): array {
|
||||
if (!$application) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'application_uuid' => (string) ($application['application_uuid'] ?? ''),
|
||||
'user_id' => (int) ($application['user_id'] ?? 0),
|
||||
'name' => (string) ($application['public_name'] ?? ''),
|
||||
'real_name' => (string) ($application['person_name_masked'] ?? ''),
|
||||
'msg' => (string) ($application['manual_reason'] ?? ''),
|
||||
'type' => (string) ($application['type'] ?? ''),
|
||||
'time' => (string) ($application['created_at'] ?? ''),
|
||||
'fee' => (string) ($application['fee_amount'] ?? '0.00'),
|
||||
'_application' => $application,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function application_view_model(array $application, int $poll_interval): array {
|
||||
$stage = (string) ($application['status'] ?? '');
|
||||
return [
|
||||
'uuid' => (string) ($application['application_uuid'] ?? ''),
|
||||
'type' => (string) ($application['type'] ?? ''),
|
||||
'status' => TransitionService::public_status($stage),
|
||||
'stage' => $stage,
|
||||
'next_action' => self::next_action($stage),
|
||||
'public_name' => (string) ($application['public_name'] ?? ''),
|
||||
'result_code' => (string) ($application['provider_result_code'] ?? ''),
|
||||
'message' => (string) ($application['provider_result_summary'] ?? ''),
|
||||
'redirect_url' => esc_url_raw((string) ($application['redirect_url'] ?? '')),
|
||||
'expires_at' => (string) ($application['expires_at'] ?? ''),
|
||||
'created_at' => (string) ($application['created_at'] ?? ''),
|
||||
'poll_interval' => $poll_interval,
|
||||
];
|
||||
}
|
||||
|
||||
public static function application_count(int $user_id, string $type): int {
|
||||
return (new ApplicationRepository())->count_user_type($user_id, sanitize_key($type));
|
||||
}
|
||||
|
||||
public static function get_status(int $user_id): string {
|
||||
return (string) (self::snapshot($user_id)['status'] ?? '');
|
||||
}
|
||||
|
||||
public static function is_verified(int $user_id): bool {
|
||||
return !empty(self::snapshot($user_id)['verified']);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public static function get_auth_info(int $user_id): array {
|
||||
$info = self::snapshot($user_id)['info'] ?? [];
|
||||
return is_array($info) ? $info : [];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public static function get_apply_payload(int $user_id): array {
|
||||
$payload = self::snapshot($user_id)['apply'] ?? [];
|
||||
return is_array($payload) ? $payload : [];
|
||||
}
|
||||
|
||||
/** @return array<int,array{id:int,url:string,thumbnail_url:string}> */
|
||||
public static function get_evidence_items(array $application, int $owner_id = 0, bool $admin_context = false): array {
|
||||
$record = is_array($application['_application'] ?? null) ? $application['_application'] : null;
|
||||
if (!$record && !empty($application['application_uuid'])) {
|
||||
$record = (new ApplicationRepository())->find_by_uuid((string) $application['application_uuid']);
|
||||
}
|
||||
return $record ? (new EvidenceService())->items($record, $admin_context) : [];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public static function application_view(array $application, int $owner_id = 0, bool $admin_context = false): array {
|
||||
return [
|
||||
'name' => (string) ($application['name'] ?? ''),
|
||||
'real_name' => (string) ($application['real_name'] ?? ''),
|
||||
'desc' => (string) ($application['desc'] ?? ''),
|
||||
'msg' => (string) ($application['msg'] ?? ''),
|
||||
'type' => sanitize_key((string) ($application['type'] ?? '')),
|
||||
'time' => (string) ($application['time'] ?? ''),
|
||||
'evidence' => self::get_evidence_items($application, $owner_id, $admin_context),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<int,array<string,mixed>> */
|
||||
public static function get_review_history(int $user_id): array {
|
||||
$application = (new ApplicationRepository())->latest_for_user($user_id);
|
||||
if (!$application) {
|
||||
return [];
|
||||
}
|
||||
$history = [];
|
||||
foreach ((new EventRepository())->for_application((int) $application['id']) as $event) {
|
||||
$history[] = [
|
||||
'status' => TransitionService::public_status((string) ($event['to_status'] ?? '')),
|
||||
'reason' => (string) ($event['result_code'] ?? ''),
|
||||
'reviewer' => (int) ($event['actor_user_id'] ?? 0),
|
||||
'time' => (string) ($event['created_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
return $history;
|
||||
}
|
||||
|
||||
/** @param array<string,string> $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_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);
|
||||
$note = sanitize_textarea_field($note);
|
||||
if (!in_array($status, ['manual_pending', 'verified', 'rejected', 'cancelled'], true)) {
|
||||
return new \WP_Error('aksrt_verify_status', __('审核状态无效', 'aksrt'));
|
||||
}
|
||||
if (in_array($status, ['rejected', 'cancelled'], true) && $note === '') {
|
||||
return new \WP_Error('aksrt_verify_note', __('请填写处理原因', 'aksrt'));
|
||||
}
|
||||
$fields = [];
|
||||
foreach (['type', 'public_name'] as $key) {
|
||||
if (array_key_exists($key, $profile_fields)) {
|
||||
$fields[$key] = (string) $profile_fields[$key];
|
||||
}
|
||||
/** @return array<string,mixed> */
|
||||
public static function get_auth_info(int $user_id): array {
|
||||
$stored = get_user_meta($user_id, 'auth_info', true);
|
||||
if (!is_array($stored)) {
|
||||
return [];
|
||||
}
|
||||
$fields['review_note'] = $note;
|
||||
$fields['reviewer_id'] = $reviewer_id;
|
||||
$result = (new TransitionService($repo))->admin_transition($application, $status, $fields, [
|
||||
'source' => 'admin',
|
||||
return [
|
||||
'name' => sanitize_text_field((string) ($stored['name'] ?? '')),
|
||||
'desc' => sanitize_textarea_field((string) ($stored['desc'] ?? '')),
|
||||
'time' => sanitize_text_field((string) ($stored['time'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
public static function meets_minimum_level(int $user_id, int $minimum_level, bool $level_enabled = true): bool {
|
||||
return $minimum_level <= 0
|
||||
|| !$level_enabled
|
||||
|| !class_exists('AKSRT\\User\\Level')
|
||||
|| Level::get_level($user_id) >= $minimum_level;
|
||||
}
|
||||
|
||||
/** @param array<string,string> $profile_fields */
|
||||
public static function review(int $user_id, string $application_uuid, string $status, string $note, int $reviewer_id, array $profile_fields) {
|
||||
$status = sanitize_key($status);
|
||||
$note = sanitize_textarea_field($note);
|
||||
if (!in_array($status, ['verified', 'rejected'], true)) {
|
||||
return new \WP_Error('aksrt_auth_status', __('审核结果无效', 'aksrt'));
|
||||
}
|
||||
if ($status === 'rejected' && $note === '') {
|
||||
return new \WP_Error('aksrt_auth_note', __('请填写驳回原因', 'aksrt'));
|
||||
}
|
||||
$repo = new ApplicationRepository();
|
||||
$application = $repo->find_by_uuid(sanitize_text_field($application_uuid));
|
||||
if (!$application || (int) $application['user_id'] !== $user_id) {
|
||||
return new \WP_Error('aksrt_auth_missing', __('认证申请不存在', 'aksrt'));
|
||||
}
|
||||
if ((string) $application['status'] !== 'manual_pending') {
|
||||
return new \WP_Error('aksrt_auth_conflict', __('该申请已处理,请刷新后重试', 'aksrt'));
|
||||
}
|
||||
$name = sanitize_text_field((string) ($profile_fields['name'] ?? $application['public_name'] ?? ''));
|
||||
$description = sanitize_textarea_field((string) ($profile_fields['description'] ?? $application['description'] ?? ''));
|
||||
if ($status === 'verified' && $name === '') {
|
||||
return new \WP_Error('aksrt_auth_name', __('请填写认证名称', 'aksrt'));
|
||||
}
|
||||
$updated = $repo->transition((int) $application['id'], 'manual_pending', $status, [
|
||||
'review_note' => $note,
|
||||
'reviewer_id' => $reviewer_id,
|
||||
'completed_at' => current_time('mysql', true),
|
||||
...($status === 'verified' ? ['public_name' => $name, 'description' => $description] : []),
|
||||
]);
|
||||
if (!$updated) {
|
||||
return new \WP_Error('aksrt_auth_conflict', __('认证状态已变化,请刷新后重试', 'aksrt'));
|
||||
}
|
||||
if ($status === 'verified') {
|
||||
update_user_meta($user_id, 'auth', 1);
|
||||
update_user_meta($user_id, 'auth_info', ['name' => $name, 'desc' => $description, 'time' => current_time('Y-m-d H:i')]);
|
||||
} else {
|
||||
delete_user_meta($user_id, 'auth');
|
||||
delete_user_meta($user_id, 'auth_info');
|
||||
}
|
||||
(new EventRepository())->record([
|
||||
'application_id' => (int) $application['id'],
|
||||
'source' => 'admin',
|
||||
'event_type' => 'manual_review',
|
||||
'dedupe_key' => hash('sha256', $application_uuid . '|review|' . $status),
|
||||
'from_status' => 'manual_pending',
|
||||
'to_status' => $status,
|
||||
'result_code' => $note,
|
||||
'actor_user_id' => $reviewer_id,
|
||||
]);
|
||||
return is_wp_error($result) ? $result : ['status' => $status, 'reviewed_at' => (string) $result['completed_at']];
|
||||
}
|
||||
|
||||
public function add_admin_user_column(array $columns): array {
|
||||
$columns['aksrt_identity_status'] = __('身份认证', 'aksrt');
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function render_admin_user_column(string $output, string $column_name, int $user_id): string {
|
||||
if ($column_name !== 'aksrt_identity_status') {
|
||||
return $output;
|
||||
}
|
||||
$status = self::get_status($user_id);
|
||||
$labels = ['pending' => __('进行中', 'aksrt'), 'verified' => __('已认证', 'aksrt'), 'rejected' => __('未通过', 'aksrt'), 'cancelled' => __('已取消', 'aksrt')];
|
||||
if ($status === '') {
|
||||
return esc_html__('未认证', 'aksrt');
|
||||
if (class_exists('AKSRT\\Messages\\Message')) {
|
||||
$title = $status === 'verified' ? __('身份认证通过', 'aksrt') : __('身份认证未通过', 'aksrt');
|
||||
$content = $status === 'verified' ? __('您的身份认证申请已通过审核', 'aksrt') : sprintf(__('您的身份认证申请未通过:%s', 'aksrt'), $note);
|
||||
\AKSRT\Messages\Message::get_instance()->send($user_id, 'auth_apply_reply', $content, [
|
||||
'title' => $title,
|
||||
'target_url' => function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('auth') : home_url('/'),
|
||||
]);
|
||||
}
|
||||
$label = (string) ($labels[$status] ?? $status);
|
||||
if ($status === 'verified') {
|
||||
$info = self::get_auth_info($user_id);
|
||||
$label .= ' · ' . self::admin_type_label((string) ($info['type'] ?? 'personal'));
|
||||
if (class_exists('AKSRT\\Settings\\MailNotifications')) {
|
||||
\AKSRT\Settings\MailNotifications::auth_reviewed($user_id, $status, $note, $reviewer_id);
|
||||
}
|
||||
return esc_html($label);
|
||||
}
|
||||
return ['status' => $status];
|
||||
}
|
||||
|
||||
public function render_admin_profile_status(\WP_User $user): void {
|
||||
$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 = current_user_can('aksrt_review_identity')
|
||||
&& current_user_can('edit_user', $user_id);
|
||||
$public_name = (string) ($application['public_name'] ?? '');
|
||||
$identity_type = in_array((string) ($application['type'] ?? ''), ['personal', 'enterprise'], true)
|
||||
? (string) $application['type']
|
||||
: 'personal';
|
||||
$selected_status = $raw_status === 'cancelled' ? '' : $raw_status;
|
||||
$status_options = [
|
||||
'' => __('未认证', 'aksrt'),
|
||||
'manual_pending' => __('待审核', 'aksrt'),
|
||||
'verified' => __('已认证', 'aksrt'),
|
||||
'rejected' => __('已驳回', 'aksrt'),
|
||||
];
|
||||
if ($raw_status !== '' && $raw_status !== 'cancelled' && !isset($status_options[$raw_status])) {
|
||||
$status_options = [$raw_status => self::admin_status_label($raw_status)] + $status_options;
|
||||
}
|
||||
?>
|
||||
<h2><?php esc_html_e('身份认证(AKSRT)', 'aksrt'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<?php if ($can_manage) : ?>
|
||||
<tr>
|
||||
<th><label for="aksrt_identity_status"><?php esc_html_e('认证状态', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<select id="aksrt_identity_status" name="aksrt_identity_status">
|
||||
<?php foreach ($status_options as $value => $label) : ?>
|
||||
<option value="<?php echo esc_attr($value); ?>" <?php selected($selected_status, $value); ?>><?php echo esc_html($label); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<input type="hidden" name="aksrt_identity_application_uuid" value="<?php echo esc_attr((string) ($application['application_uuid'] ?? '')); ?>">
|
||||
<?php wp_nonce_field('aksrt_identity_profile_' . $user_id, 'aksrt_identity_profile_nonce'); ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="aksrt_identity_type"><?php esc_html_e('认证类型', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<select id="aksrt_identity_type" name="aksrt_identity_type">
|
||||
<option value="personal" <?php selected($identity_type, 'personal'); ?>><?php esc_html_e('个人认证', 'aksrt'); ?></option>
|
||||
<option value="enterprise" <?php selected($identity_type, 'enterprise'); ?>><?php esc_html_e('企业认证', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="aksrt_identity_public_name"><?php esc_html_e('姓名 / 主体全称', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<input type="text" class="regular-text" id="aksrt_identity_public_name" name="aksrt_identity_public_name" maxlength="64" value="<?php echo esc_attr($public_name); ?>">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="aksrt_identity_review_note"><?php esc_html_e('处理说明', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<textarea class="large-text" id="aksrt_identity_review_note" name="aksrt_identity_review_note" rows="2"></textarea>
|
||||
<p class="description"><?php esc_html_e('改为“已驳回”或“未认证”时必须填写处理说明。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php elseif ($application !== null) : ?>
|
||||
<tr>
|
||||
<th><?php esc_html_e('认证状态', 'aksrt'); ?></th>
|
||||
<td><strong><?php echo esc_html(self::admin_status_label($raw_status)); ?></strong></td>
|
||||
</tr>
|
||||
<tr><th><?php esc_html_e('认证类型', 'aksrt'); ?></th><td><?php echo esc_html(self::admin_type_label($identity_type)); ?></td></tr>
|
||||
<?php if ($public_name !== '') : ?><tr><th><?php esc_html_e('姓名 / 主体全称', 'aksrt'); ?></th><td><?php echo esc_html($public_name); ?></td></tr><?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function handle_admin_profile_update(int $user_id): void {
|
||||
public function render_admin_profile(\WP_User $user): void {
|
||||
if (
|
||||
!current_user_can('aksrt_review_identity')
|
||||
|| !current_user_can('edit_user', $user_id)
|
||||
|| !isset($_POST['aksrt_identity_profile_nonce'])
|
||||
empty(self::settings()['enabled'])
|
||||
|| !current_user_can('aksrt_review_identity')
|
||||
|| !current_user_can('edit_user', (int) $user->ID)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$nonce = sanitize_text_field((string) wp_unslash($_POST['aksrt_identity_profile_nonce']));
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_identity_profile_' . $user_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$repo = new ApplicationRepository();
|
||||
$snapshot = $repo->snapshot_for_user($user_id);
|
||||
$application = is_array($snapshot['verified'] ?? null)
|
||||
? $snapshot['verified']
|
||||
: (is_array($snapshot['latest'] ?? null) ? $snapshot['latest'] : null);
|
||||
$application_uuid = sanitize_text_field((string) wp_unslash($_POST['aksrt_identity_application_uuid'] ?? ''));
|
||||
$current_uuid = (string) ($application['application_uuid'] ?? '');
|
||||
if (!hash_equals($current_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, '');
|
||||
$identity_type = sanitize_key((string) wp_unslash($_POST['aksrt_identity_type'] ?? ''));
|
||||
$status = sanitize_key((string) wp_unslash($_POST['aksrt_identity_status'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) wp_unslash($_POST['aksrt_identity_review_note'] ?? ''));
|
||||
if (!in_array($identity_type, ['personal', 'enterprise'], true)) {
|
||||
$this->set_admin_profile_notice('error', __('认证类型无效。', 'aksrt'));
|
||||
return;
|
||||
}
|
||||
$fields = [
|
||||
'type' => $identity_type,
|
||||
'public_name' => $public_name,
|
||||
];
|
||||
|
||||
$current_status = (string) ($application['status'] ?? '');
|
||||
if (!in_array($status, ['', 'manual_pending', 'verified', 'rejected'], true) && $status !== $current_status) {
|
||||
$this->set_admin_profile_notice('error', __('认证状态无效。', 'aksrt'));
|
||||
return;
|
||||
}
|
||||
$target_status = $status === '' ? 'cancelled' : $status;
|
||||
if ($target_status === 'verified' && $public_name === '') {
|
||||
$this->set_admin_profile_notice('error', __('设为已认证时请填写姓名 / 主体全称。', 'aksrt'));
|
||||
return;
|
||||
}
|
||||
if (in_array($target_status, ['rejected', 'cancelled'], true) && $target_status !== $current_status && $note === '') {
|
||||
$this->set_admin_profile_notice('error', __('改为已驳回或未认证时请填写处理说明。', 'aksrt'));
|
||||
return;
|
||||
}
|
||||
|
||||
if ($application === null && $status === '') {
|
||||
return;
|
||||
}
|
||||
if ($application === null) {
|
||||
$application = $this->create_admin_application($repo, $user_id, $fields);
|
||||
if (is_wp_error($application)) {
|
||||
$this->set_admin_profile_notice('error', $application->get_error_message());
|
||||
return;
|
||||
}
|
||||
$application_uuid = (string) $application['application_uuid'];
|
||||
$current_status = (string) $application['status'];
|
||||
}
|
||||
|
||||
if ($target_status === $current_status) {
|
||||
$updated = $repo->update((int) $application['id'], $fields);
|
||||
$this->set_admin_profile_notice($updated ? 'success' : 'error', $updated ? __('身份认证资料已更新。', 'aksrt') : __('身份认证资料更新失败。', 'aksrt'));
|
||||
return;
|
||||
}
|
||||
|
||||
$result = self::review($user_id, $application_uuid, $target_status, $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')
|
||||
);
|
||||
$info = self::get_auth_info((int) $user->ID);
|
||||
$name = (string) ($info['name'] ?? '');
|
||||
$description = (string) ($info['desc'] ?? '');
|
||||
$time = (string) ($info['time'] ?? '');
|
||||
?>
|
||||
<h2><?php esc_html_e('身份认证', 'aksrt'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr><th><label for="aksrt_auth_enabled"><?php esc_html_e('已认证用户', 'aksrt'); ?></label></th><td><input id="aksrt_auth_enabled" type="checkbox" name="aksrt_auth_enabled" value="1" <?php checked(self::is_verified((int) $user->ID)); ?>></td></tr>
|
||||
<tr><th><label for="aksrt_auth_name"><?php esc_html_e('认证名称', 'aksrt'); ?></label></th><td><input id="aksrt_auth_name" class="regular-text" type="text" maxlength="64" name="aksrt_auth_name" value="<?php echo esc_attr($name); ?>"></td></tr>
|
||||
<tr><th><label for="aksrt_auth_desc"><?php esc_html_e('认证简介', 'aksrt'); ?></label></th><td><textarea id="aksrt_auth_desc" class="large-text" rows="2" name="aksrt_auth_desc"><?php echo esc_textarea($description); ?></textarea></td></tr>
|
||||
<tr><th><label for="aksrt_auth_time"><?php esc_html_e('认证时间', 'aksrt'); ?></label></th><td><input id="aksrt_auth_time" class="regular-text" type="text" name="aksrt_auth_time" value="<?php echo esc_attr($time); ?>"></td></tr>
|
||||
</table>
|
||||
<?php wp_nonce_field('aksrt_auth_profile_' . (int) $user->ID, 'aksrt_auth_profile_nonce'); ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
/** @param array<string,string> $fields */
|
||||
private function create_admin_application(ApplicationRepository $repo, int $user_id, array $fields) {
|
||||
$application_uuid = wp_generate_uuid4();
|
||||
$client_request_id = wp_generate_uuid4();
|
||||
return $repo->create([
|
||||
'application_uuid' => $application_uuid,
|
||||
'client_request_id' => $client_request_id,
|
||||
'request_payload_hash' => hash('sha256', 'admin-profile|' . $user_id . '|' . $client_request_id),
|
||||
'user_id' => $user_id,
|
||||
'type' => $fields['type'],
|
||||
'channel' => 'manual',
|
||||
'provider' => '',
|
||||
'status' => 'created',
|
||||
'public_name' => $fields['public_name'],
|
||||
'fee_amount' => '0.00',
|
||||
'fee_status' => 'not_required',
|
||||
'attempt_ordinal' => $repo->count_user_type($user_id, $fields['type']) + 1,
|
||||
public function save_admin_profile(int $user_id): void {
|
||||
if (
|
||||
empty(self::settings()['enabled'])
|
||||
|| !current_user_can('aksrt_review_identity')
|
||||
|| !current_user_can('edit_user', $user_id)
|
||||
|| !isset($_POST['aksrt_auth_profile_nonce'])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$nonce = sanitize_text_field((string) wp_unslash($_POST['aksrt_auth_profile_nonce']));
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_auth_profile_' . $user_id)) return;
|
||||
if (empty($_POST['aksrt_auth_enabled'])) {
|
||||
delete_user_meta($user_id, 'auth');
|
||||
delete_user_meta($user_id, 'auth_info');
|
||||
return;
|
||||
}
|
||||
$name = sanitize_text_field((string) wp_unslash($_POST['aksrt_auth_name'] ?? ''));
|
||||
if ($name === '') return;
|
||||
update_user_meta($user_id, 'auth', 1);
|
||||
update_user_meta($user_id, 'auth_info', [
|
||||
'name' => $name,
|
||||
'desc' => sanitize_textarea_field((string) wp_unslash($_POST['aksrt_auth_desc'] ?? '')),
|
||||
'time' => sanitize_text_field((string) wp_unslash($_POST['aksrt_auth_time'] ?? '')) ?: current_time('Y-m-d H:i'),
|
||||
]);
|
||||
}
|
||||
|
||||
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 '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p>' . esc_html((string) $notice['message']) . '</p></div>';
|
||||
}
|
||||
|
||||
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_type_label(string $type): string {
|
||||
return $type === 'enterprise' ? __('企业认证', 'aksrt') : __('个人认证', 'aksrt');
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user