fix(identity): complete native certification review flow

This commit is contained in:
kate522 committed 2026-07-29 12:25:58 +08:00
1 parent 32658be87d
commit 2dfaa10798
16 files changed
+571 -1167

No files matched your search

+197 -19
View File
@@ -11,10 +11,14 @@ defined('ABSPATH') || exit;
class AuthApply {
public const STATUS_META = 'aksrt_user_verify_status';
public const REASON_META = 'aksrt_user_verify_reason';
public const APPLY_META = 'aksrt_user_verify_apply';
public const INFO_META = 'aksrt_user_verify_info';
public const STATUS_META = 'aksrt_user_verify_status';
public const REASON_META = 'aksrt_user_verify_reason';
public const APPLY_META = 'aksrt_user_verify_apply';
public const INFO_META = 'aksrt_user_verify_info';
public const REQUESTED_AT_META = 'aksrt_user_verify_requested_at';
public const REVIEWED_AT_META = 'aksrt_user_verify_reviewed_at';
public const REVIEWER_META = 'aksrt_user_verify_reviewer';
public const HISTORY_META = 'aksrt_user_verify_history';
private static $instance = null;
@@ -27,6 +31,7 @@ class AuthApply {
public function init(): void {
add_action('wp_ajax_aksrt_user_auth_apply', [$this, 'ajax_apply']);
add_action('admin_post_aksrt_verify_evidence', [$this, 'serve_evidence']);
}
/**
@@ -98,44 +103,209 @@ class AuthApply {
}
/**
* Build the only public representation of certification evidence.
* Return certification evidence only to its owner or an administrator.
*
* @param array<string,mixed> $application
* @return array<int,array{id:int,url:string,thumbnail_url:string}>
*/
public static function get_evidence_items(array $application): array {
public static function get_evidence_items(array $application, int $owner_id = 0, bool $admin_context = false): array {
if ($owner_id <= 0) {
return [];
}
$can_view = $admin_context
? current_user_can('manage_options')
: get_current_user_id() === $owner_id;
if (!$can_view) {
return [];
}
$items = [];
foreach ((array) ($application['evidence_attachment_ids'] ?? []) as $attachment_id) {
$attachment_id = (int) $attachment_id;
if ($attachment_id <= 0 || get_post_type($attachment_id) !== 'attachment') {
if (
$attachment_id <= 0
|| get_post_type($attachment_id) !== 'attachment'
|| (int) get_post_meta($attachment_id, '_aksrt_verify_evidence_owner', true) !== $owner_id
) {
continue;
}
$url = wp_get_attachment_url($attachment_id);
if (!is_string($url) || $url === '') {
continue;
}
$thumb = wp_get_attachment_image_url($attachment_id, 'thumbnail');
$url = add_query_arg([
'action' => 'aksrt_verify_evidence',
'attachment' => $attachment_id,
'_wpnonce' => wp_create_nonce('aksrt_verify_evidence_' . $attachment_id),
], admin_url('admin-post.php'));
$items[] = [
'id' => $attachment_id,
'url' => esc_url_raw($url),
'thumbnail_url' => esc_url_raw(is_string($thumb) && $thumb !== '' ? $thumb : $url),
'thumbnail_url' => esc_url_raw($url),
];
}
return $items;
}
public function serve_evidence(): void {
if (!is_user_logged_in()) {
wp_die(esc_html__('请先登录', 'aksrt'), '', ['response' => 401]);
}
$attachment_id = isset($_GET['attachment']) ? (int) $_GET['attachment'] : 0;
if ($attachment_id <= 0 || !check_admin_referer('aksrt_verify_evidence_' . $attachment_id)) {
wp_die(esc_html__('安全校验失败', 'aksrt'), '', ['response' => 403]);
}
$owner_id = (int) get_post_meta($attachment_id, '_aksrt_verify_evidence_owner', true);
if ($owner_id <= 0 || (get_current_user_id() !== $owner_id && !current_user_can('manage_options'))) {
wp_die(esc_html__('您没有权限查看该证明材料', 'aksrt'), '', ['response' => 403]);
}
$file_path = get_attached_file($attachment_id);
if (!is_string($file_path) || $file_path === '' || !is_file($file_path) || !is_readable($file_path)) {
wp_die(esc_html__('证明材料不存在', 'aksrt'), '', ['response' => 404]);
}
$mime = (string) get_post_mime_type($attachment_id);
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
wp_die(esc_html__('证明材料类型无效', 'aksrt'), '', ['response' => 415]);
}
nocache_headers();
header('Content-Type: ' . $mime);
header('Content-Disposition: inline; filename="' . rawurlencode(basename($file_path)) . '"');
header('Content-Length: ' . (string) filesize($file_path));
readfile($file_path);
exit;
}
/** @param array<string,mixed> $application */
public static function application_view(array $application): array {
public static function application_view(array $application, int $owner_id = 0, bool $admin_context = false): array {
return [
'name' => (string) ($application['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),
'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 {
$history = get_user_meta($user_id, self::HISTORY_META, true);
$history = is_array($history) ? array_values($history) : [];
usort($history, static fn(array $left, array $right): int => strcmp(
(string) ($right['time'] ?? ''),
(string) ($left['time'] ?? '')
));
return $history;
}
/**
* Apply one authoritative certification status transition.
*
* @return array<string,mixed>|\WP_Error
*/
public static function review(int $user_id, string $status, string $note, int $reviewer_id) {
$status = sanitize_key($status);
$note = sanitize_textarea_field($note);
if (!in_array($status, ['verified', 'rejected', 'cancelled'], true)) {
return new \WP_Error('aksrt_verify_status', __('审核状态无效', 'aksrt'));
}
if ($user_id <= 0 || !get_userdata($user_id) || $reviewer_id <= 0) {
return new \WP_Error('aksrt_verify_user', __('用户不存在', 'aksrt'));
}
if ($status === 'cancelled' && $note === '') {
return new \WP_Error('aksrt_verify_note', __('取消认证时必须填写原因', 'aksrt'));
}
if ($status === 'rejected' && $note === '') {
return new \WP_Error('aksrt_verify_note', __('驳回申请时必须填写原因', 'aksrt'));
}
$current = self::get_status($user_id);
$allowed = $status === 'cancelled'
? $current === 'verified'
: $current === 'pending';
$application = self::get_apply_payload($user_id);
if (!$allowed || $application === [] || empty($application['name']) || empty($application['type'])) {
return new \WP_Error('aksrt_verify_transition', __('该认证记录当前不可执行此操作', 'aksrt'));
}
global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT umeta_id, meta_value
FROM {$wpdb->usermeta}
WHERE user_id = %d AND meta_key = %s
ORDER BY umeta_id ASC",
$user_id,
self::STATUS_META
), ARRAY_A);
if (count((array) $rows) !== 1 || sanitize_key((string) ($rows[0]['meta_value'] ?? '')) !== $current) {
return new \WP_Error('aksrt_verify_state', __('认证状态数据异常,请修复后重试', 'aksrt'));
}
$updated = $wpdb->query($wpdb->prepare(
"UPDATE {$wpdb->usermeta}
SET meta_value = %s
WHERE umeta_id = %d AND meta_value = %s",
$status,
(int) $rows[0]['umeta_id'],
$current
));
if ($updated !== 1) {
return new \WP_Error('aksrt_verify_conflict', __('认证状态已变化,请刷新后重试', 'aksrt'));
}
$reviewed_at = current_time('mysql');
update_user_meta($user_id, self::REVIEWED_AT_META, $reviewed_at);
update_user_meta($user_id, self::REVIEWER_META, $reviewer_id);
if ($note !== '') {
update_user_meta($user_id, self::REASON_META, $note);
} else {
delete_user_meta($user_id, self::REASON_META);
}
if ($status === 'verified') {
update_user_meta($user_id, self::INFO_META, [
'name' => (string) ($application['name'] ?? ''),
'desc' => (string) ($application['desc'] ?? ''),
'type' => sanitize_key((string) ($application['type'] ?? '')),
'time' => current_time('Y-m-d H:i'),
]);
} elseif ($status === 'cancelled') {
delete_user_meta($user_id, self::INFO_META);
}
$entry = [
'status' => $status,
'reason' => $note,
'reviewer' => $reviewer_id,
'time' => $reviewed_at,
'application' => self::application_view($application),
];
$history = self::get_review_history($user_id);
array_unshift($history, $entry);
update_user_meta($user_id, self::HISTORY_META, array_slice($history, 0, 50));
self::send_review_notification($user_id, $status, $note);
do_action('aksrt_user_auth_reviewed', $user_id, $status, $note, $reviewer_id);
return ['status' => $status, 'reviewed_at' => $reviewed_at];
}
private static function send_review_notification(int $user_id, string $status, string $note): void {
if (!class_exists('\\AKSRT\\Messages\\Message')) {
return;
}
if ($status === 'verified') {
$message = __('您的官方认证申请已通过审核', 'aksrt');
$title = __('官方认证审核结果', 'aksrt');
} elseif ($status === 'cancelled') {
$message = sprintf(__('您的官方认证已被取消:%s', 'aksrt'), $note);
$title = __('官方认证已取消', 'aksrt');
} else {
$message = sprintf(__('您的官方认证申请未通过:%s', 'aksrt'), $note);
$title = __('官方认证审核结果', 'aksrt');
}
\AKSRT\Messages\Message::get_instance()->send($user_id, 'auth_apply_reply', $message, [
'title' => $title,
'target_url' => aksrt_get_user_center_tab_url('auth'),
]);
}
public function ajax_apply(): void {
if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') {
wp_send_json_error(['message' => __('请求方式无效', 'aksrt')], 405);
@@ -222,13 +392,14 @@ class AuthApply {
$this->renew_submission_lock($lock_key, $lock_token);
$previous_payload = self::get_apply_payload($user_id);
$requested_at = current_time('Y-m-d H:i');
$payload = [
'name' => $name,
'desc' => $desc,
'msg' => $msg,
'type' => $type,
'evidence_attachment_ids' => $uploaded,
'time' => current_time('Y-m-d H:i'),
'time' => $requested_at,
];
$payload_saved = update_user_meta($user_id, self::APPLY_META, $payload);
@@ -246,14 +417,17 @@ class AuthApply {
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('认证申请保存失败,请重试', 'aksrt')], 500);
}
update_user_meta($user_id, self::REQUESTED_AT_META, $requested_at);
delete_user_meta($user_id, self::REVIEWED_AT_META);
delete_user_meta($user_id, self::REVIEWER_META);
delete_user_meta($user_id, self::REASON_META);
if (!$this->owns_submission_lock($lock_key, $lock_token)) {
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('认证申请已提交,请刷新查看最新状态', 'aksrt')], 409);
}
$this->renew_submission_lock($lock_key, $lock_token);
delete_user_meta($user_id, self::REASON_META);
$this->delete_attachments(array_column(self::get_evidence_items($previous_payload), 'id'));
$this->delete_attachments(array_column(self::get_evidence_items($previous_payload, $user_id), 'id'));
/** @param int $user_id @param array $payload */
do_action('aksrt_user_auth_apply_submitted', $user_id, $payload);
@@ -314,7 +488,11 @@ class AuthApply {
$this->delete_attachments($attachment_ids);
return new \WP_Error('aksrt_auth_upload_type', __('证明材料类型校验失败', 'aksrt'));
}
wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
wp_update_post([
'ID' => $attachment_id,
'post_author' => $user_id,
'post_status' => 'private',
]);
update_post_meta($attachment_id, '_aksrt_verify_evidence_owner', $user_id);
$attachment_ids[] = $attachment_id;
}