fix(user): repair phone binding and profile fields

This commit is contained in:
2026-10-05 20:55:20 +08:00
parent 0cf4639d95
commit f646fa8573
15 changed files with 627 additions and 162 deletions
@@ -131,7 +131,7 @@ trait UserControllerAuthPhoneOperations {
private function auth_find_user(string $target, string $channel) {
if ($channel === 'phone') {
$users = get_users([
'meta_key' => 'aksrt_phone',
'meta_key' => 'phone_number',
'meta_value' => $target,
'number' => 1,
]);
+1 -1
View File
@@ -50,7 +50,7 @@ final class Service {
&& \AKSRT\Settings\Settings::signin_phone_enabled()
) {
$users = get_users([
'meta_key' => 'aksrt_phone',
'meta_key' => 'phone_number',
'meta_value' => $login,
'number' => 1,
]);
+13 -11
View File
@@ -127,7 +127,7 @@ trait ServicePart1b {
}
// 绑定手机号
update_user_meta($user_id, 'aksrt_phone', $phone);
update_user_meta($user_id, 'phone_number', $phone);
update_user_meta($user_id, 'aksrt_phone_verified', '1');
// 自动登录
@@ -173,21 +173,23 @@ trait ServicePart1b {
return ['success' => false, 'error' => __('验证码错误或已过期', 'aksrt')];
}
// 检查手机号是否已被其他用户绑定
$user_id = get_current_user_id();
$existing = self::get_user_by_phone($phone);
if ($existing && (int) $existing->ID !== get_current_user_id()) {
if ($existing && (int) $existing->ID !== $user_id) {
return ['success' => false, 'error' => __('该手机号已被其他账号绑定', 'aksrt')];
}
if ($existing && (int) $existing->ID === $user_id) {
return ['success' => false, 'error' => __('手机号不能与当前绑定号码相同', 'aksrt')];
}
$user_id = get_current_user_id();
update_user_meta($user_id, 'aksrt_phone', $phone);
update_user_meta($user_id, 'aksrt_phone_verified', '1');
do_action('aksrt_user_phone_changed', $user_id, $phone);
return [
$old_phone = (string) get_user_meta($user_id, 'phone_number', true);
update_user_meta($user_id, 'phone_number', $phone);
update_user_meta($user_id, 'aksrt_phone_verified', '1');
do_action('aksrt_user_phone_changed', $user_id, $phone, $old_phone);
return [
'success' => true,
'message' => __('手机号绑定成功', 'aksrt'),
'message' => $old_phone !== '' ? __('手机号修改成功', 'aksrt') : __('手机号绑定成功', 'aksrt'),
];
}
@@ -196,7 +198,7 @@ trait ServicePart1b {
*/
private static function get_user_by_phone(string $phone): ?\WP_User {
$users = get_users([
'meta_key' => 'aksrt_phone',
'meta_key' => 'phone_number',
'meta_value' => $phone,
'number' => 1,
'fields' => 'all',
+282
View File
@@ -0,0 +1,282 @@
<?php
/** Native WordPress profile fields for AKSRT user data. */
namespace AKSRT\User;
defined('ABSPATH') || exit;
final class ProfileAdmin {
private const PROFILE_NONCE = 'aksrt_profile_admin_nonce';
private const PHONE_META = 'phone_number';
public static function init(): void {
if (!is_admin()) {
return;
}
add_action('show_user_profile', [self::class, 'render']);
add_action('edit_user_profile', [self::class, 'render']);
add_action('personal_options_update', [self::class, 'save']);
add_action('edit_user_profile_update', [self::class, 'save']);
add_filter('user_profile_update_errors', [self::class, 'validate'], 10, 3);
add_action('admin_footer-user-edit.php', [self::class, 'unlock_script']);
add_action('admin_footer-profile.php', [self::class, 'unlock_script']);
}
public static function render(\WP_User $user): void {
if (!current_user_can('edit_user', (int) $user->ID)) {
return;
}
$user_id = (int) $user->ID;
$details = function_exists('aksrt_get_user_profile_details')
? aksrt_get_user_profile_details($user_id)
: ['gender' => 'secret', 'website' => '', 'qq' => '', 'wechat' => '', 'weibo' => '', 'github' => '', 'address' => ''];
$avatar_url = function_exists('aksrt_get_custom_avatar_url') ? aksrt_get_custom_avatar_url($user_id, 96) : '';
$cover_id = (int) get_user_meta($user_id, 'aksrt_background_attachment_id', true);
$cover_url = esc_url_raw((string) get_user_meta($user_id, 'aksrt_background_image', true));
if ($cover_url === '' && $cover_id > 0) $cover_url = (string) wp_get_attachment_image_url($cover_id, 'full');
$phone = (string) get_user_meta($user_id, self::PHONE_META, true);
?>
<h2 id="aksrt-more-profile-data"><?php esc_html_e('更多资料(AKSRT)', 'aksrt'); ?></h2>
<table class="form-table" role="presentation">
<?php if (!function_exists('aksrt_is_avatar_upload_enabled') || aksrt_is_avatar_upload_enabled()) : ?><tr>
<th scope="row"><label for="aksrt_avatar_file"><?php esc_html_e('自定义头像', 'aksrt'); ?></label></th>
<td>
<?php echo get_avatar($user_id, 96, '', $user->display_name, ['loading' => 'eager']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<p><input id="aksrt_avatar_file" type="file" name="aksrt_avatar_file" accept="image/jpeg,image/png,image/webp,image/gif"></p>
<?php if ($avatar_url !== '') : ?><label><input type="checkbox" name="aksrt_avatar_reset" value="1"> <?php esc_html_e('恢复项目默认头像并删除当前头像文件', 'aksrt'); ?></label><?php endif; ?>
<p class="description"><?php esc_html_e('支持 JPG、PNG、GIF、WebP;大小上限由“权限 → 附件权限”控制。', 'aksrt'); ?></p>
<?php wp_nonce_field('aksrt_save_admin_avatar_' . $user_id, 'aksrt_admin_avatar_nonce'); ?>
</td>
</tr><?php endif; ?>
<?php if (!function_exists('aksrt_is_user_cover_upload_enabled') || aksrt_is_user_cover_upload_enabled()) : ?>
<tr>
<th scope="row"><label for="aksrt_cover_file"><?php esc_html_e('自定义封面', 'aksrt'); ?></label></th>
<td>
<?php if ($cover_url !== '') : ?><img src="<?php echo esc_url($cover_url); ?>" alt="" style="display:block;max-width:360px;height:auto;margin-bottom:8px;border-radius:4px;"><?php endif; ?>
<p><input id="aksrt_cover_file" type="file" name="aksrt_cover_file" accept="image/jpeg,image/png,image/webp,image/gif"></p>
<?php if ($cover_url !== '') : ?><label><input type="checkbox" name="aksrt_background_reset" value="1"> <?php esc_html_e('恢复默认封面并删除当前封面文件', 'aksrt'); ?></label><?php endif; ?>
<p class="description"><?php esc_html_e('用户主页资料卡使用的背景图。', 'aksrt'); ?></p>
</td>
</tr>
<?php endif; ?>
<tr>
<th scope="row"><label for="aksrt_profile_gender"><?php esc_html_e('性别', 'aksrt'); ?></label></th>
<td><select id="aksrt_profile_gender" name="aksrt_profile_gender"><option value="secret" <?php selected($details['gender'], 'secret'); ?>><?php esc_html_e('保密', 'aksrt'); ?></option><option value="male" <?php selected($details['gender'], 'male'); ?>><?php esc_html_e('男', 'aksrt'); ?></option><option value="female" <?php selected($details['gender'], 'female'); ?>><?php esc_html_e('女', 'aksrt'); ?></option></select></td>
</tr>
<tr><th scope="row"><label for="aksrt_profile_qq">QQ</label></th><td><input id="aksrt_profile_qq" class="regular-text" type="text" name="aksrt_profile_qq" maxlength="50" value="<?php echo esc_attr($details['qq']); ?>"></td></tr>
<tr><th scope="row"><label for="aksrt_profile_wechat"><?php esc_html_e('微信', 'aksrt'); ?></label></th><td><input id="aksrt_profile_wechat" class="regular-text" type="text" name="aksrt_profile_wechat" maxlength="100" value="<?php echo esc_attr($details['wechat']); ?>"></td></tr>
<tr><th scope="row"><label for="aksrt_profile_weibo"><?php esc_html_e('微博', 'aksrt'); ?></label></th><td><input id="aksrt_profile_weibo" class="regular-text" type="url" name="aksrt_profile_weibo" value="<?php echo esc_attr($details['weibo']); ?>" placeholder="https://"></td></tr>
<tr><th scope="row"><label for="aksrt_profile_github">GitHub</label></th><td><input id="aksrt_profile_github" class="regular-text" type="url" name="aksrt_profile_github" value="<?php echo esc_attr($details['github']); ?>" placeholder="https://github.com/"></td></tr>
<tr><th scope="row"><label for="aksrt_profile_website"><?php esc_html_e('个人网站', 'aksrt'); ?></label></th><td><input id="aksrt_profile_website" class="regular-text" type="url" name="aksrt_profile_website" value="<?php echo esc_attr($details['website']); ?>" placeholder="https://"></td></tr>
<tr><th scope="row"><label for="aksrt_profile_address"><?php esc_html_e('住址', 'aksrt'); ?></label></th><td><input id="aksrt_profile_address" class="regular-text" type="text" name="aksrt_profile_address" maxlength="200" value="<?php echo esc_attr($details['address']); ?>"></td></tr>
<tr>
<th scope="row"><label for="user_phone_number"><?php esc_html_e('绑定手机号', 'aksrt'); ?></label></th>
<td>
<input id="user_phone_number" class="regular-text" type="text" name="phone_number" value="<?php echo esc_attr($phone); ?>" readonly>
<input type="hidden" name="aksrt_phone_edit_unlocked" id="aksrt_phone_edit_unlocked" value="0">
<p class="description"><?php esc_html_e('手机号涉及登录功能,默认只读。管理员确认后可直接修改。', 'aksrt'); ?></p>
<button type="button" class="button" data-aksrt-unlock-phone><?php esc_html_e('修改用户手机号', 'aksrt'); ?></button>
</td>
</tr>
</table>
<?php wp_nonce_field(self::PROFILE_NONCE . '_' . $user_id, self::PROFILE_NONCE); ?>
<?php
}
public static function validate(\WP_Error $errors, bool $update, ?\WP_User $user = null): void {
$user_id = $user instanceof \WP_User ? (int) $user->ID : (int) ($_POST['user_id'] ?? 0);
if ($user_id <= 0 || !current_user_can('edit_user', $user_id) || empty($_POST[self::PROFILE_NONCE])) {
return;
}
$nonce = sanitize_text_field(wp_unslash($_POST[self::PROFILE_NONCE]));
if (!wp_verify_nonce($nonce, self::PROFILE_NONCE . '_' . $user_id)) {
$errors->add('aksrt_profile_nonce', __('更多资料安全验证失败,请刷新页面重试。', 'aksrt'));
return;
}
$phone = trim(sanitize_text_field(wp_unslash($_POST['phone_number'] ?? '')));
$old_phone = (string) get_user_meta($user_id, self::PHONE_META, true);
$unlocked = !empty($_POST['aksrt_phone_edit_unlocked']);
if ($phone !== '' && $phone !== $old_phone && $unlocked) {
if (!class_exists('AKSRT\\Sms\\SMS') || !\AKSRT\Sms\SMS::is_phonenumber($phone)) {
$errors->add('aksrt_profile_phone', __('绑定手机号格式不正确。', 'aksrt'));
} else {
$users = get_users(['meta_key' => self::PHONE_META, 'meta_value' => $phone, 'exclude' => [$user_id], 'number' => 1, 'fields' => 'ids']);
if (!empty($users)) {
$errors->add('aksrt_profile_phone_exists', __('该手机号已被其他账号绑定。', 'aksrt'));
}
}
}
foreach (['aksrt_profile_weibo' => '微博', 'aksrt_profile_github' => 'GitHub', 'aksrt_profile_website' => '个人网站'] as $key => $label) {
$value = trim((string) wp_unslash($_POST[$key] ?? ''));
if ($value !== '' && !esc_url_raw($value)) {
$errors->add('aksrt_profile_' . sanitize_key($label), sprintf(__('%s地址格式不正确。', 'aksrt'), $label));
}
}
$gender = sanitize_key(wp_unslash($_POST['aksrt_profile_gender'] ?? 'secret'));
if (!in_array($gender, ['secret', 'male', 'female'], true)) {
$errors->add('aksrt_profile_gender', __('性别选项无效。', 'aksrt'));
}
$details_limits = ['aksrt_profile_qq' => 50, 'aksrt_profile_wechat' => 100, 'aksrt_profile_address' => 200];
foreach ($details_limits as $key => $limit) {
if (mb_strlen((string) wp_unslash($_POST[$key] ?? '')) > $limit) {
$errors->add('aksrt_profile_' . $key, sprintf(__('资料字段不能超过 %d 个字符。', 'aksrt'), $limit));
}
}
if (!function_exists('aksrt_is_avatar_upload_enabled') || aksrt_is_avatar_upload_enabled()) {
self::validate_image_upload($errors, 'aksrt_avatar_file', __('头像', 'aksrt'), $user_id);
}
if (!function_exists('aksrt_is_user_cover_upload_enabled') || aksrt_is_user_cover_upload_enabled()) {
self::validate_image_upload($errors, 'aksrt_cover_file', __('封面', 'aksrt'), $user_id);
}
}
public static function save(int $user_id): void {
if ($user_id <= 0 || !current_user_can('edit_user', $user_id)) {
return;
}
$nonce = sanitize_text_field(wp_unslash($_POST[self::PROFILE_NONCE] ?? ''));
if (!wp_verify_nonce($nonce, self::PROFILE_NONCE . '_' . $user_id)) {
return;
}
$details = function_exists('aksrt_get_user_profile_details') ? aksrt_get_user_profile_details($user_id) : [];
if (isset($_POST['aksrt_profile_gender'])) $details['gender'] = sanitize_key(wp_unslash($_POST['aksrt_profile_gender']));
if (isset($_POST['aksrt_profile_qq'])) $details['qq'] = sanitize_text_field(wp_unslash($_POST['aksrt_profile_qq']));
if (isset($_POST['aksrt_profile_wechat'])) $details['wechat'] = sanitize_text_field(wp_unslash($_POST['aksrt_profile_wechat']));
if (isset($_POST['aksrt_profile_weibo'])) $details['weibo'] = esc_url_raw(wp_unslash($_POST['aksrt_profile_weibo']));
if (isset($_POST['aksrt_profile_github'])) $details['github'] = esc_url_raw(wp_unslash($_POST['aksrt_profile_github']));
if (isset($_POST['aksrt_profile_website'])) $details['website'] = esc_url_raw(wp_unslash($_POST['aksrt_profile_website']));
if (isset($_POST['aksrt_profile_address'])) $details['address'] = sanitize_text_field(wp_unslash($_POST['aksrt_profile_address']));
if (function_exists('aksrt_get_user_profile_details')) update_user_meta($user_id, 'aksrt_profile_details', $details);
$old_phone = (string) get_user_meta($user_id, self::PHONE_META, true);
$new_phone = trim(sanitize_text_field(wp_unslash($_POST['phone_number'] ?? '')));
if (!empty($_POST['aksrt_phone_edit_unlocked']) && $new_phone !== '' && $new_phone !== $old_phone && class_exists('AKSRT\\Sms\\SMS') && \AKSRT\Sms\SMS::is_phonenumber($new_phone)) {
$users = get_users(['meta_key' => self::PHONE_META, 'meta_value' => $new_phone, 'exclude' => [$user_id], 'number' => 1, 'fields' => 'ids']);
if (empty($users)) {
update_user_meta($user_id, self::PHONE_META, $new_phone);
update_user_meta($user_id, 'aksrt_phone_verified', '1');
do_action('aksrt_user_phone_changed', $user_id, $new_phone, $old_phone);
}
}
self::save_avatar($user_id);
self::save_cover($user_id);
}
private static function validate_image_upload(\WP_Error $errors, string $field, string $label, int $user_id): void {
if (empty($_FILES[$field]['name'])) return;
$file = $_FILES[$field];
$max_size = class_exists('AKSRT\\Attachment\\UploadLimits')
? \AKSRT\Attachment\UploadLimits::limit_bytes('image', $user_id)
: (int) wp_max_upload_size();
$actual_size = !empty($file['tmp_name']) && is_readable($file['tmp_name']) ? (int) filesize($file['tmp_name']) : 0;
if ((int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || $actual_size <= 0 || $actual_size > $max_size) {
$errors->add('aksrt_profile_' . sanitize_key($field) . '_size', sprintf(__('%1$s文件无效或超过 %2$s。', 'aksrt'), $label, size_format($max_size)));
return;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
$checked = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], ['jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp']);
$image_size = wp_getimagesize($file['tmp_name']);
if (empty($checked['type']) || empty($checked['ext']) || !wp_match_mime_types('image', $checked['type']) || !$image_size || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) {
$errors->add('aksrt_profile_' . sanitize_key($field) . '_type', sprintf(__('%s必须是有效的 JPG、PNG、GIF 或 WebP 图片,且总像素不能超过 4000 万。', 'aksrt'), $label));
}
}
private static function save_avatar(int $user_id): void {
if (function_exists('aksrt_is_avatar_upload_enabled') && !aksrt_is_avatar_upload_enabled()) return;
$nonce = sanitize_text_field(wp_unslash($_POST['aksrt_admin_avatar_nonce'] ?? ''));
if (!wp_verify_nonce($nonce, 'aksrt_save_admin_avatar_' . $user_id)) return;
$previous_id = (int) get_user_meta($user_id, 'aksrt_avatar_attachment_id', true);
if (!empty($_POST['aksrt_avatar_reset'])) {
delete_user_meta($user_id, 'aksrt_avatar_attachment_id');
if (function_exists('aksrt_delete_owned_avatar_attachment')) aksrt_delete_owned_avatar_attachment($previous_id, $user_id);
do_action('aksrt_user_avatar_changed', $user_id, 0, '');
return;
}
if (empty($_FILES['aksrt_avatar_file']['name'])) 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_id = media_handle_upload('aksrt_avatar_file', 0, [], ['test_form' => false, 'mimes' => ['jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp']]);
if (is_wp_error($attachment_id) || !wp_attachment_is_image((int) $attachment_id)) {
if (!is_wp_error($attachment_id)) wp_delete_attachment((int) $attachment_id, true);
return;
}
$attachment_id = (int) $attachment_id;
wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
update_post_meta($attachment_id, '_aksrt_profile_media_user_id', $user_id);
update_post_meta($attachment_id, '_aksrt_profile_media_type', 'avatar');
update_user_meta($user_id, 'aksrt_avatar_attachment_id', $attachment_id);
if (function_exists('aksrt_delete_owned_avatar_attachment')) aksrt_delete_owned_avatar_attachment($previous_id, $user_id);
do_action('aksrt_user_avatar_changed', $user_id, $attachment_id, (string) wp_get_attachment_image_url($attachment_id, 'thumbnail'));
}
private static function save_cover(int $user_id): void {
if (function_exists('aksrt_is_user_cover_upload_enabled') && !aksrt_is_user_cover_upload_enabled()) return;
$old_id = (int) get_user_meta($user_id, 'aksrt_background_attachment_id', true);
if (!empty($_POST['aksrt_background_reset'])) {
delete_user_meta($user_id, 'aksrt_background_attachment_id');
delete_user_meta($user_id, 'aksrt_background_image');
self::delete_attachment($old_id, $user_id);
return;
}
if (empty($_FILES['aksrt_cover_file']['name'])) return;
$file = $_FILES['aksrt_cover_file'];
$max_size = class_exists('AKSRT\\Attachment\\UploadLimits') ? \AKSRT\Attachment\UploadLimits::limit_bytes('image', $user_id) : (int) wp_max_upload_size();
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || empty($file['tmp_name']) || filesize($file['tmp_name']) > $max_size) 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';
$checked = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], ['jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp']);
$image_size = wp_getimagesize($file['tmp_name']);
if (empty($checked['type']) || empty($checked['ext']) || !$image_size || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) return;
$attachment_id = media_handle_upload('aksrt_cover_file', 0, [], ['test_form' => false, 'mimes' => ['jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp']]);
if (is_wp_error($attachment_id) || !wp_attachment_is_image((int) $attachment_id)) {
if (!is_wp_error($attachment_id)) wp_delete_attachment((int) $attachment_id, true);
return;
}
$attachment_id = (int) $attachment_id;
wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
update_post_meta($attachment_id, '_aksrt_profile_media_user_id', $user_id);
update_post_meta($attachment_id, '_aksrt_profile_media_type', 'background');
$url = wp_get_attachment_image_url($attachment_id, 'full');
if (!$url) {
wp_delete_attachment($attachment_id, true);
return;
}
update_user_meta($user_id, 'aksrt_background_attachment_id', $attachment_id);
update_user_meta($user_id, 'aksrt_background_image', esc_url_raw($url));
self::delete_attachment($old_id, $user_id);
}
private static function delete_attachment(int $attachment_id, int $user_id): void {
$attachment = $attachment_id > 0 ? get_post($attachment_id) : null;
$managed_user_id = $attachment_id > 0 ? (int) get_post_meta($attachment_id, '_aksrt_profile_media_user_id', true) : 0;
$managed_type = $attachment_id > 0 ? (string) get_post_meta($attachment_id, '_aksrt_profile_media_type', true) : '';
if ($attachment instanceof \WP_Post && $attachment->post_type === 'attachment' && (int) $attachment->post_author === $user_id && $managed_user_id === $user_id && $managed_type === 'background') {
wp_delete_attachment($attachment_id, true);
}
}
public static function unlock_script(): void {
if (!get_current_screen() || !in_array(get_current_screen()->base, ['profile', 'user-edit'], true)) return;
?>
<script>
document.addEventListener('click', function (event) {
var button = event.target.closest('[data-aksrt-unlock-phone]');
if (!button) return;
var field = document.getElementById('user_phone_number');
var flag = document.getElementById('aksrt_phone_edit_unlocked');
if (!field || !flag) return;
field.removeAttribute('readonly');
field.focus();
flag.value = '1';
button.disabled = true;
});
</script>
<?php
}
}