feat(user): add local avatar management flow
This commit is contained in:
@@ -371,14 +371,6 @@ class ImportExportController extends ControllerBase {
|
||||
'status' => $backfill_done ? 'ok' : 'info',
|
||||
];
|
||||
|
||||
// Avatar source
|
||||
$avatar_source = \AKSRT\Settings\Settings::get_all()['avatar_source'] ?? 'cn.cravatar.com';
|
||||
$checks['avatar_source'] = [
|
||||
'label' => '头像服务',
|
||||
'value' => $avatar_source,
|
||||
'status' => $avatar_source === 'cn.cravatar.com' ? 'ok' : 'info',
|
||||
];
|
||||
|
||||
// Option count
|
||||
$option_count = (int) $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE 'aksrt_%'"
|
||||
|
||||
@@ -26,13 +26,6 @@ class GlobalSchema {
|
||||
['key' => 'performance', 'title' => '性能优化', 'fields' => [
|
||||
['key' => 'lazyload_enabled', 'label' => '图片懒加载', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'avatar', 'title' => '头像服务', 'fields' => [
|
||||
['key' => 'avatar_source', 'label' => '头像服务源', 'type' => 'select', 'default' => 'cn.cravatar.com', 'options' => [
|
||||
['label' => 'Cravatar (国内推荐)', 'value' => 'cn.cravatar.com'],
|
||||
['label' => 'Gravatar 国内镜像', 'value' => 'cn.gravatar.com'],
|
||||
['label' => 'Gravatar 官方', 'value' => 'gravatar.com'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'filing', 'title' => '备案信息', 'fields' => [
|
||||
['key' => 'icp_filing', 'label' => 'ICP 备案号', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'footer_text', 'label' => '页脚附加文字', 'type' => 'input', 'default' => ''],
|
||||
@@ -203,13 +196,6 @@ class GlobalSchema {
|
||||
['label' => '样式 10', 'value' => 'no10'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'avatar', 'title' => '头像设置', 'fields' => [
|
||||
['key' => 'avatar_source', 'label' => '头像服务源', 'type' => 'select', 'default' => 'cn.cravatar.com', 'options' => [
|
||||
['label' => 'Cravatar (国内推荐)', 'value' => 'cn.cravatar.com'],
|
||||
['label' => 'Gravatar 国内镜像', 'value' => 'cn.gravatar.com'],
|
||||
['label' => 'Gravatar 官方', 'value' => 'gravatar.com'],
|
||||
], 'help' => '国内站点建议优先选 Cravatar'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'code-highlight' => [
|
||||
|
||||
@@ -516,7 +516,6 @@ class SchemaUsers {
|
||||
'groups' => [
|
||||
['key' => 'center', 'title' => '用户中心', 'fields' => [
|
||||
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,posts,comments,favorites,vip,credits,auth', 'help' => '逗号分隔。auth=官方认证;vip、订单和认证会按功能开关自动注入'],
|
||||
['key' => 'user_avatar_upload', 'label' => '头像上传', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_cover_upload', 'label' => '封面上传', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'],
|
||||
@@ -527,7 +526,6 @@ class SchemaUsers {
|
||||
['label' => '国家+省份', 'value' => 'country_province'],
|
||||
]],
|
||||
['key' => 'user_join_day_my_name', 'label' => '站名', 'type' => 'input', 'default' => '本站', 'desc' => '用于「已加入 X 天」显示中的站点名称'],
|
||||
['key' => 'user_avatar_default', 'label' => '默认头像', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'user_cover_img', 'label' => '默认封面', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'user_rewrite_slug', 'label' => '用户页slug', 'type' => 'input', 'default' => 'user'],
|
||||
['key' => 'user_follow_s', 'label' => '启用关注功能', 'type' => 'switch', 'default' => true],
|
||||
|
||||
@@ -52,9 +52,16 @@ class UserController extends ControllerBase {
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/user/avatar', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_upload_user_avatar'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_upload_user_avatar'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
],
|
||||
[
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'rest_delete_user_avatar'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
],
|
||||
]);
|
||||
|
||||
// Comment inline image upload (logged-in users)
|
||||
|
||||
@@ -19,6 +19,23 @@ trait UserControllerProfileMediaOperations {
|
||||
);
|
||||
}
|
||||
|
||||
public function rest_delete_user_avatar(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
return $this->error(__('请先登录', 'aksrt'), 401);
|
||||
}
|
||||
|
||||
$attachment_id = (int) get_user_meta($user_id, 'aksrt_avatar_attachment_id', true);
|
||||
delete_user_meta($user_id, 'aksrt_avatar_attachment_id');
|
||||
$this->delete_owned_profile_attachment($attachment_id, $user_id);
|
||||
do_action('aksrt_user_avatar_changed', $user_id, 0, '');
|
||||
|
||||
return $this->success([
|
||||
'message' => __('已恢复默认头像', 'aksrt'),
|
||||
'url' => function_exists('aksrt_get_default_avatar_url') ? aksrt_get_default_avatar_url() : '',
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_upload_user_background(\WP_REST_Request $request): \WP_REST_Response {
|
||||
return $this->upload_profile_image(
|
||||
['background', 'file'],
|
||||
@@ -58,15 +75,39 @@ trait UserControllerProfileMediaOperations {
|
||||
return $this->error(__('请选择要上传的图片', 'aksrt'), 400);
|
||||
}
|
||||
|
||||
$file = $_FILES[$file_key];
|
||||
if (!empty($file['size']) && (int) $file['size'] > 2 * MB_IN_BYTES) {
|
||||
return $this->error(__('图片大小不能超过 2MB', 'aksrt'), 413);
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
|
||||
$file = $_FILES[$file_key];
|
||||
$upload_error = isset($file['error']) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE;
|
||||
if ($upload_error !== UPLOAD_ERR_OK) {
|
||||
return $this->error(__('头像文件上传失败,请重新选择', 'aksrt'), 400);
|
||||
}
|
||||
|
||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
$max_size_mb = max(1, min(10, (int) ($settings['avatar_max_size'] ?? 2)));
|
||||
$max_size = $max_size_mb * MB_IN_BYTES;
|
||||
$actual_size = !empty($file['tmp_name']) && is_readable($file['tmp_name']) ? (int) filesize($file['tmp_name']) : 0;
|
||||
if ($actual_size <= 0 || $actual_size > $max_size) {
|
||||
return $this->error(sprintf(__('图片大小必须在 1 字节至 %dMB 之间', 'aksrt'), $max_size_mb), 413);
|
||||
}
|
||||
|
||||
$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',
|
||||
]);
|
||||
if (empty($checked['type']) || empty($checked['ext']) || !wp_match_mime_types('image', $checked['type'])) {
|
||||
return $this->error(__('仅支持 JPG、PNG、GIF 或 WebP 图片', 'aksrt'), 415);
|
||||
}
|
||||
|
||||
$image_size = wp_getimagesize($file['tmp_name']);
|
||||
if (!$image_size || empty($image_size[0]) || empty($image_size[1]) || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) {
|
||||
return $this->error(__('图片无效或尺寸过大', 'aksrt'), 415);
|
||||
}
|
||||
|
||||
$attachment_id = media_handle_upload($file_key, 0, [], [
|
||||
'test_form' => false,
|
||||
'mimes' => [
|
||||
@@ -81,6 +122,14 @@ trait UserControllerProfileMediaOperations {
|
||||
}
|
||||
|
||||
$attachment_id = (int) $attachment_id;
|
||||
wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
|
||||
if (!wp_attachment_is_image($attachment_id)) {
|
||||
wp_delete_attachment($attachment_id, true);
|
||||
return $this->error(__('上传文件不是有效图片', 'aksrt'), 415);
|
||||
}
|
||||
update_post_meta($attachment_id, '_aksrt_profile_media_user_id', $user_id);
|
||||
update_post_meta($attachment_id, '_aksrt_profile_media_type', $is_avatar ? 'avatar' : 'background');
|
||||
|
||||
$url = wp_get_attachment_image_url($attachment_id, $is_avatar ? 'thumbnail' : 'full');
|
||||
if (!$url) {
|
||||
wp_delete_attachment($attachment_id, true);
|
||||
@@ -94,10 +143,11 @@ trait UserControllerProfileMediaOperations {
|
||||
}
|
||||
|
||||
if ($previous_id > 0 && $previous_id !== $attachment_id) {
|
||||
$previous = get_post($previous_id);
|
||||
if ($previous instanceof \WP_Post && (int) $previous->post_author === $user_id) {
|
||||
wp_delete_attachment($previous_id, true);
|
||||
}
|
||||
$this->delete_owned_profile_attachment($previous_id, $user_id);
|
||||
}
|
||||
|
||||
if ($is_avatar) {
|
||||
do_action('aksrt_user_avatar_changed', $user_id, $attachment_id, $url);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
@@ -109,4 +159,22 @@ trait UserControllerProfileMediaOperations {
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function delete_owned_profile_attachment(int $attachment_id, int $user_id): bool {
|
||||
if ($attachment_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$attachment = get_post($attachment_id);
|
||||
if (!$attachment instanceof \WP_Post || $attachment->post_type !== 'attachment' || (int) $attachment->post_author !== $user_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$managed_user_id = (int) get_post_meta($attachment_id, '_aksrt_profile_media_user_id', true);
|
||||
if ($managed_user_id > 0 && $managed_user_id !== $user_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) wp_delete_attachment($attachment_id, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,6 @@ trait DisplayPageGroups {
|
||||
'logo_text' => self::logo_text(),
|
||||
'logo_width' => self::logo_width(),
|
||||
'site_favicon' => self::favicon_url(),
|
||||
'avatar_source' => self::avatar_source(),
|
||||
'icp_filing' => self::icp_filing(),
|
||||
'footer_text' => self::footer_text(),
|
||||
'theme_mode' => self::theme_mode(),
|
||||
|
||||
@@ -502,15 +502,6 @@ trait FacadeBulk {
|
||||
|
||||
|
||||
|
||||
/** 获取头像镜像源 */
|
||||
public static function avatar_source(): string {
|
||||
$source = sanitize_text_field((string) self::get_value('avatar_source', 'cn.cravatar.com'));
|
||||
$allowed = ['cn.cravatar.com', 'cn.gravatar.com', 'gravatar.com'];
|
||||
return in_array($source, $allowed, true) ? $source : 'cn.cravatar.com';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 获取SEO关键词 */
|
||||
public static function seo_keywords(): string {
|
||||
$s = self::get_all();
|
||||
|
||||
@@ -132,7 +132,7 @@ class UserRuntime {
|
||||
|
||||
return [
|
||||
'tabs' => $normalized_tabs,
|
||||
'avatar_upload' => !isset($settings['user_avatar_upload']) || !empty($settings['user_avatar_upload']),
|
||||
'avatar_upload' => !isset($settings['allow_avatar_upload']) || !empty($settings['allow_avatar_upload']),
|
||||
'cover_upload' => !isset($settings['user_cover_upload']) || !empty($settings['user_cover_upload']),
|
||||
'social_links' => !isset($settings['user_social_links']) || !empty($settings['user_social_links']),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user