fix(user-center): complete certification and order flows

Fix modal scroll ownership and certification modal styling, store evidence as Media Library attachments, reconcile missed provider callbacks, validate currencies, and complete digital fulfillment only after durable entitlement delivery.
This commit is contained in:
kate522 committed 2026-07-29 10:29:59 +08:00
1 parent b945c6e835
commit 815e83bac0
26 files changed
+613 -148

No files matched your search

+238 -78
View File
@@ -1,6 +1,6 @@
<?php
/**
* Frontend identity verification apply (zibll user-auth page parity on aksrt_user_verify_* meta).
* Frontend identity verification application.
*
* @package AKSRT
*/
@@ -85,59 +85,89 @@ class AuthApply {
return self::get_status($user_id) === 'verified';
}
/**
* @return array<string,mixed>
*/
/** @return array<string,mixed> */
public static function get_auth_info(int $user_id): array {
$info = get_user_meta($user_id, self::INFO_META, true);
return is_array($info) ? $info : [];
}
/**
* @return array<string,mixed>
*/
/** @return array<string,mixed> */
public static function get_apply_payload(int $user_id): array {
$apply = get_user_meta($user_id, self::APPLY_META, true);
return is_array($apply) ? $apply : [];
}
public static function evidence_src(string $value): string {
if (preg_match('#^data:image/(?:jpeg|png|gif|webp);base64,[A-Za-z0-9+/=]+$#', $value)) {
return $value;
/**
* Build the only public representation of certification evidence.
*
* @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 {
$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') {
continue;
}
$url = wp_get_attachment_url($attachment_id);
if (!is_string($url) || $url === '') {
continue;
}
$thumb = wp_get_attachment_image_url($attachment_id, 'thumbnail');
$items[] = [
'id' => $attachment_id,
'url' => esc_url_raw($url),
'thumbnail_url' => esc_url_raw(is_string($thumb) && $thumb !== '' ? $thumb : $url),
];
}
return esc_url_raw($value);
return $items;
}
/** @param array<string,mixed> $application */
public static function application_view(array $application): 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),
];
}
public function ajax_apply(): void {
if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') {
wp_send_json_error(['message' => __('请求方式无效', 'aksrt')], 405);
}
if (!is_user_logged_in()) {
wp_send_json(['error' => 1, 'msg' => __('请先登录', 'aksrt')]);
wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
}
if (!check_ajax_referer('aksrt_user_auth_apply', '_wpnonce', false)) {
wp_send_json(['error' => 1, 'msg' => __('安全校验失败', 'aksrt')]);
wp_send_json_error(['message' => __('安全校验失败', 'aksrt')], 403);
}
$user_id = get_current_user_id();
$s = self::settings();
if (!$s['enabled'] || !$s['allow_apply']) {
wp_send_json(['error' => 1, 'msg' => __('暂不能申请身份认证', 'aksrt')]);
wp_send_json_error(['message' => __('暂不能申请身份认证', 'aksrt')], 403);
}
if (self::is_verified($user_id)) {
wp_send_json(['error' => 1, 'msg' => __('您已是认证用户', 'aksrt')]);
wp_send_json_error(['message' => __('您已是认证用户', 'aksrt')], 409);
}
if (self::get_status($user_id) === 'pending') {
wp_send_json(['error' => 1, 'msg' => __('您的申请正在审核中', 'aksrt')]);
wp_send_json_error(['message' => __('您的申请正在审核中', 'aksrt')], 409);
}
if ($s['limit_level'] > 0 && class_exists('\AKSRT\User\Level')) {
$lv = (int) Level::get_level($user_id);
if ($lv < $s['limit_level']) {
wp_send_json([
'error' => 1,
'msg' => sprintf(
wp_send_json_error([
'message' => sprintf(
/* translators: %d: required level */
__('等级不足,需达到 LV.%d 后申请', 'aksrt'),
$s['limit_level']
),
]);
], 403);
}
}
@@ -146,77 +176,207 @@ class AuthApply {
$msg = isset($_POST['msg']) ? sanitize_textarea_field(wp_unslash((string) $_POST['msg'])) : '';
$type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : '';
$allowed_types = array_column((array) $s['types'], 'slug');
$name_length = function_exists('mb_strlen') ? mb_strlen($name) : strlen($name);
if ($name_length < 4 || $name_length > 16) {
wp_send_json(['error' => 1, 'msg' => __('认证身份长度需在 4-16 个字符之间', 'aksrt')]);
if ($this->text_length($name) < 4 || $this->text_length($name) > 16) {
wp_send_json_error(['message' => __('认证身份长度需在 4-16 个字符之间', 'aksrt')], 422);
}
if ($this->text_length($desc) > 300 || $this->text_length($msg) > 500) {
wp_send_json_error(['message' => __('认证说明内容过长', 'aksrt')], 422);
}
if ($type === '' || !in_array($type, $allowed_types, true)) {
wp_send_json(['error' => 1, 'msg' => __('请选择有效的认证类型', 'aksrt')]);
wp_send_json_error(['message' => __('请选择有效的认证类型', 'aksrt')], 422);
}
$images = [];
if (!empty($_FILES['image']['name'])) {
$files = $_FILES['image'];
$names = is_array($files['name']) ? $files['name'] : [$files['name']];
if (!is_array($files['name'])) {
$files = [
'name' => $names,
'type' => [$files['type']],
'tmp_name' => [$files['tmp_name']],
'error' => [$files['error']],
'size' => [$files['size']],
];
$lock_key = 'aksrt_auth_apply_lock_' . $user_id;
$lock_token = wp_generate_uuid4();
$lock_payload = ['token' => $lock_token, 'expires' => time() + 300];
if (!add_option($lock_key, $lock_payload, '', false)) {
$current_lock = get_option($lock_key, []);
if (is_array($current_lock) && (int) ($current_lock['expires'] ?? 0) < time()) {
delete_option($lock_key);
}
if (count($files['name']) > 3) {
wp_send_json(['error' => 1, 'msg' => __('证明材料最多上传 3 张', 'aksrt')]);
}
foreach ($files['name'] as $index => $filename) {
if ($filename === '') {
continue;
}
if ((int) ($files['error'][$index] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
wp_send_json(['error' => 1, 'msg' => __('证明材料上传失败,请重新选择', 'aksrt')]);
}
$size = (int) ($files['size'][$index] ?? 0);
$tmp_name = (string) ($files['tmp_name'][$index] ?? '');
if ($size <= 0 || $size > 2 * MB_IN_BYTES || !is_uploaded_file($tmp_name)) {
wp_send_json(['error' => 1, 'msg' => __('单张证明图片不能超过 2MB', 'aksrt')]);
}
$file_check = wp_check_filetype_and_ext($tmp_name, (string) $filename);
$mime = (string) ($file_check['type'] ?? '');
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
wp_send_json(['error' => 1, 'msg' => __('证明材料仅支持 JPG、PNG、GIF 或 WebP 图片', 'aksrt')]);
}
$bytes = file_get_contents($tmp_name);
if (!is_string($bytes) || $bytes === '') {
wp_send_json(['error' => 1, 'msg' => __('证明材料读取失败', 'aksrt')]);
}
$images[] = 'data:' . $mime . ';base64,' . base64_encode($bytes);
if (!add_option($lock_key, $lock_payload, '', false)) {
wp_send_json_error(['message' => __('认证申请正在提交,请勿重复操作', 'aksrt')], 409);
}
}
$uploaded = [];
if (self::is_verified($user_id)) {
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('您已是认证用户', 'aksrt')], 409);
}
if (self::get_status($user_id) === 'pending') {
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('您的申请正在审核中', 'aksrt')], 409);
}
$uploaded = $this->upload_evidence($user_id);
if (is_wp_error($uploaded)) {
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => $uploaded->get_error_message()], 422);
}
if (!$this->owns_submission_lock($lock_key, $lock_token)) {
$this->delete_attachments($uploaded);
wp_send_json_error(['message' => __('认证申请提交超时,请重新提交', 'aksrt')], 409);
}
$this->renew_submission_lock($lock_key, $lock_token);
$previous_payload = self::get_apply_payload($user_id);
$payload = [
'name' => $name,
'desc' => $desc,
'msg' => $msg,
'type' => $type,
'img' => $images,
'time' => current_time('Y-m-d H:i'),
'name' => $name,
'desc' => $desc,
'msg' => $msg,
'type' => $type,
'evidence_attachment_ids' => $uploaded,
'time' => current_time('Y-m-d H:i'),
];
update_user_meta($user_id, self::APPLY_META, $payload);
update_user_meta($user_id, self::STATUS_META, 'pending');
$payload_saved = update_user_meta($user_id, self::APPLY_META, $payload);
if ($payload_saved === false && get_user_meta($user_id, self::APPLY_META, true) !== $payload) {
$this->delete_attachments($uploaded);
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('认证申请保存失败,请重试', 'aksrt')], 500);
}
$status_saved = update_user_meta($user_id, self::STATUS_META, 'pending');
if ($status_saved === false && self::get_status($user_id) !== 'pending') {
$this->delete_attachments($uploaded);
$previous_payload === []
? delete_user_meta($user_id, self::APPLY_META)
: update_user_meta($user_id, self::APPLY_META, $previous_payload);
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('认证申请保存失败,请重试', 'aksrt')], 500);
}
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'));
/**
* @param int $user_id
* @param array $payload
*/
/** @param int $user_id @param array $payload */
do_action('aksrt_user_auth_apply_submitted', $user_id, $payload);
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json([
'error' => 0,
'msg' => __('提交成功,请等待管理员审核', 'aksrt'),
'reload' => true,
]);
wp_send_json_success([
'message' => __('提交成功,请等待管理员审核', 'aksrt'),
'status' => 'pending',
], 201);
}
/** @return array<int,int>|\WP_Error */
private function upload_evidence(int $user_id) {
$files = $this->normalize_uploads($_FILES['image'] ?? null);
if (is_wp_error($files)) {
return $files;
}
if ($files === []) {
return [];
}
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$attachment_ids = [];
foreach ($files as $file) {
$filename = sanitize_file_name((string) ($file['name'] ?? ''));
$tmp_name = (string) ($file['tmp_name'] ?? '');
$size = (int) ($file['size'] ?? 0);
$error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE);
if ($error !== UPLOAD_ERR_OK || $filename === '' || $size <= 0 || $size > 2 * MB_IN_BYTES || !is_uploaded_file($tmp_name)) {
$this->delete_attachments($attachment_ids);
return new \WP_Error('aksrt_auth_upload_failed', __('证明材料上传失败,单张图片不能超过 2MB', 'aksrt'));
}
$file_check = wp_check_filetype_and_ext($tmp_name, $filename);
$mime = (string) ($file_check['type'] ?? '');
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
$this->delete_attachments($attachment_ids);
return new \WP_Error('aksrt_auth_upload_type', __('证明材料仅支持 JPG、PNG、GIF 或 WebP 图片', 'aksrt'));
}
$attachment_id = media_handle_sideload([
'name' => $filename,
'tmp_name' => $tmp_name,
'type' => $mime,
'error' => UPLOAD_ERR_OK,
'size' => $size,
], 0, __('官方认证证明材料', 'aksrt'));
if (is_wp_error($attachment_id)) {
$this->delete_attachments($attachment_ids);
return new \WP_Error('aksrt_auth_upload_failed', __('证明材料写入媒体库失败', 'aksrt'));
}
$attachment_id = (int) $attachment_id;
$stored_mime = (string) get_post_mime_type($attachment_id);
if (!in_array($stored_mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
wp_delete_attachment($attachment_id, true);
$this->delete_attachments($attachment_ids);
return new \WP_Error('aksrt_auth_upload_type', __('证明材料类型校验失败', 'aksrt'));
}
wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
update_post_meta($attachment_id, '_aksrt_verify_evidence_owner', $user_id);
$attachment_ids[] = $attachment_id;
}
return $attachment_ids;
}
/** @return array<int,array<string,mixed>>|\WP_Error */
private function normalize_uploads($input) {
if (!is_array($input) || empty($input['name'])) {
return [];
}
$names = is_array($input['name']) ? $input['name'] : [$input['name']];
if (count($names) > 3) {
return new \WP_Error('aksrt_auth_upload_limit', __('证明材料最多上传 3 张', 'aksrt'));
}
$files = [];
foreach ($names as $index => $name) {
if ((string) $name === '') {
continue;
}
$files[] = [
'name' => $name,
'type' => is_array($input['type'] ?? null) ? ($input['type'][$index] ?? '') : ($input['type'] ?? ''),
'tmp_name' => is_array($input['tmp_name'] ?? null) ? ($input['tmp_name'][$index] ?? '') : ($input['tmp_name'] ?? ''),
'error' => is_array($input['error'] ?? null) ? ($input['error'][$index] ?? UPLOAD_ERR_NO_FILE) : ($input['error'] ?? UPLOAD_ERR_NO_FILE),
'size' => is_array($input['size'] ?? null) ? ($input['size'][$index] ?? 0) : ($input['size'] ?? 0),
];
}
return $files;
}
/** @param array<int,int|string> $attachment_ids */
private function delete_attachments(array $attachment_ids): void {
foreach ($attachment_ids as $attachment_id) {
$attachment_id = (int) $attachment_id;
if ($attachment_id > 0 && get_post_type($attachment_id) === 'attachment') {
wp_delete_attachment($attachment_id, true);
}
}
}
private function owns_submission_lock(string $lock_key, string $lock_token): bool {
$current_lock = get_option($lock_key, []);
return is_array($current_lock)
&& (int) ($current_lock['expires'] ?? 0) >= time()
&& hash_equals((string) ($current_lock['token'] ?? ''), $lock_token);
}
private function renew_submission_lock(string $lock_key, string $lock_token): void {
if ($this->owns_submission_lock($lock_key, $lock_token)) {
update_option($lock_key, ['token' => $lock_token, 'expires' => time() + 300], false);
}
}
private function release_submission_lock(string $lock_key, string $lock_token): void {
$current_lock = get_option($lock_key, []);
if (is_array($current_lock) && hash_equals((string) ($current_lock['token'] ?? ''), $lock_token)) {
delete_option($lock_key);
}
}
private function text_length(string $value): int {
return function_exists('mb_strlen') ? mb_strlen($value) : strlen($value);
}
}