refactor(classes): organize inc/classes into domain folders
Move ~94 root class shells and root *-traits into domain directories (core, auth, user, credits, content, payment/gateways, forum, shop, sms, ai, messages, extensions, widgets, settings, admin). Update bootstrap require paths, co-locate traits with owners, fix forum/user relative requires, drop duplicate shop controller requires, and wire home-modules load. Path-only; class names and init order unchanged.
This commit is contained in:
1 parent
2a8fe68048
commit
ce1c13b947
115 files changed
+171
-224
No files matched your search
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Follow — 关注系统
|
||||
*
|
||||
* 使用专用表 {prefix}aksrt_follow 存储关注关系。
|
||||
* 支持:关注、取消关注、计数、是否已关注。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Follow {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function table(): string {
|
||||
return AKSRT_DB_Migrator::table('follow');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关注用户
|
||||
*
|
||||
* @param int $follower_id 关注者 ID
|
||||
* @param int $following_id 被关注者 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function follow(int $follower_id, int $following_id): bool {
|
||||
if ($follower_id <= 0 || $following_id <= 0 || $follower_id === $following_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 已关注则跳过
|
||||
if ($this->is_following($follower_id, $following_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$result = $wpdb->insert($this->table(), [
|
||||
'follower_id' => $follower_id,
|
||||
'following_id' => $following_id,
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%d', '%d', '%s']);
|
||||
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
do_action('aksrt_user_followed', $follower_id, $following_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消关注
|
||||
*/
|
||||
public function unfollow(int $follower_id, int $following_id): bool {
|
||||
if ($follower_id <= 0 || $following_id <= 0) return false;
|
||||
|
||||
global $wpdb;
|
||||
$result = $wpdb->delete($this->table(), [
|
||||
'follower_id' => $follower_id,
|
||||
'following_id' => $following_id,
|
||||
], ['%d', '%d']);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换关注状态
|
||||
*
|
||||
* @return bool true=已关注, false=已取消
|
||||
*/
|
||||
public function toggle(int $follower_id, int $following_id): bool {
|
||||
if ($this->is_following($follower_id, $following_id)) {
|
||||
$this->unfollow($follower_id, $following_id);
|
||||
return false;
|
||||
}
|
||||
$this->follow($follower_id, $following_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已关注
|
||||
*/
|
||||
public function is_following(int $follower_id, int $following_id): bool {
|
||||
if ($follower_id <= 0 || $following_id <= 0) return false;
|
||||
|
||||
global $wpdb;
|
||||
$exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$this->table()} WHERE follower_id = %d AND following_id = %d LIMIT 1",
|
||||
$follower_id, $following_id
|
||||
));
|
||||
|
||||
return (int) $exists > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取粉丝数(谁关注了 target_id)
|
||||
*/
|
||||
public function get_follower_count(int $user_id): int {
|
||||
if ($user_id <= 0) return 0;
|
||||
|
||||
global $wpdb;
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table()} WHERE following_id = %d",
|
||||
$user_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关注数(target_id 关注了谁)
|
||||
*/
|
||||
public function get_following_count(int $user_id): int {
|
||||
if ($user_id <= 0) return 0;
|
||||
|
||||
global $wpdb;
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table()} WHERE follower_id = %d",
|
||||
$user_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取粉丝列表
|
||||
*/
|
||||
public function get_followers(int $user_id, int $page = 1, int $per_page = 20): array {
|
||||
return $this->get_user_list($user_id, 'following_id', $page, $per_page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关注列表
|
||||
*/
|
||||
public function get_following(int $user_id, int $page = 1, int $per_page = 20): array {
|
||||
return $this->get_user_list($user_id, 'follower_id', $page, $per_page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用用户列表查询
|
||||
*/
|
||||
private function get_user_list(int $user_id, string $where_field, int $page, int $per_page): array {
|
||||
global $wpdb;
|
||||
|
||||
$other_field = $where_field === 'follower_id' ? 'following_id' : 'follower_id';
|
||||
$offset = max(0, ($page - 1) * $per_page);
|
||||
|
||||
$total = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table()} WHERE {$where_field} = %d",
|
||||
$user_id
|
||||
));
|
||||
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT {$other_field} as user_id, created_at
|
||||
FROM {$this->table()}
|
||||
WHERE {$where_field} = %d
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %d OFFSET %d",
|
||||
$user_id, $per_page, $offset
|
||||
), ARRAY_A);
|
||||
|
||||
$items = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$uid = (int) $row['user_id'];
|
||||
$user = get_userdata($uid);
|
||||
$items[] = [
|
||||
'user_id' => $uid,
|
||||
'display_name' => $user ? $user->display_name : '',
|
||||
'avatar_url' => $user ? get_avatar_url($uid, ['size' => 48]) : '',
|
||||
'created_at' => $row['created_at'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'total_pages' => max(1, (int) ceil($total / $per_page)),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin grant/revoke badges on WP user profile screens.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_User_Badges_Admin {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_action('show_user_profile', [$this, 'render_section']);
|
||||
add_action('edit_user_profile', [$this, 'render_section']);
|
||||
add_action('personal_options_update', [$this, 'handle_update']);
|
||||
add_action('edit_user_profile_update', [$this, 'handle_update']);
|
||||
}
|
||||
|
||||
private function can_manage(): bool {
|
||||
return class_exists('AKSRT_User_Badges') && AKSRT_User_Badges::can_manually_set(get_current_user_id());
|
||||
}
|
||||
|
||||
public function render_section(WP_User $user): void {
|
||||
if (!class_exists('AKSRT_User_Badges') || !AKSRT_User_Badges::enabled()) {
|
||||
return;
|
||||
}
|
||||
$dict = AKSRT_User_Badges::get_dictionary();
|
||||
if (!$dict) {
|
||||
return;
|
||||
}
|
||||
$owned = AKSRT_User_Badges::get_owned((int) $user->ID);
|
||||
$wear = AKSRT_User_Badges::get_wear((int) $user->ID);
|
||||
$can = $this->can_manage();
|
||||
?>
|
||||
<a id="aksrt-user-badges"></a>
|
||||
<h2><?php esc_html_e('AKSRT 徽章', 'aksrt'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th><label><?php esc_html_e('已授予徽章', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<?php if (!$can) : ?>
|
||||
<p class="description"><?php esc_html_e('当前账号无权授予/收回徽章。', 'aksrt'); ?></p>
|
||||
<?php endif; ?>
|
||||
<fieldset <?php disabled(!$can); ?>>
|
||||
<?php foreach ($dict as $item) : ?>
|
||||
<?php
|
||||
$slug = $item['slug'];
|
||||
$checked = in_array($slug, $owned, true);
|
||||
?>
|
||||
<label style="display:inline-flex;align-items:center;gap:6px;margin:0 16px 10px 0;">
|
||||
<input type="checkbox" name="aksrt_user_badges[]" value="<?php echo esc_attr($slug); ?>" <?php checked($checked); ?> <?php disabled(!$can); ?>>
|
||||
<?php if (!empty($item['icon_url'])) : ?>
|
||||
<img src="<?php echo esc_url($item['icon_url']); ?>" alt="" width="18" height="18" style="object-fit:contain;vertical-align:middle;">
|
||||
<?php endif; ?>
|
||||
<span><?php echo esc_html($item['label']); ?> <code><?php echo esc_html($slug); ?></code></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
<p class="description"><?php esc_html_e('勾选即授予;取消勾选并保存即收回。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="aksrt_wear_badge"><?php esc_html_e('当前佩戴', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<select name="aksrt_wear_badge" id="aksrt_wear_badge" <?php disabled(!$can); ?>>
|
||||
<option value=""><?php esc_html_e('(未佩戴)', 'aksrt'); ?></option>
|
||||
<?php foreach ($dict as $item) : ?>
|
||||
<?php if (!in_array($item['slug'], $owned, true) && $item['slug'] !== $wear) : ?>
|
||||
<?php continue; ?>
|
||||
<?php endif; ?>
|
||||
<option value="<?php echo esc_attr($item['slug']); ?>" <?php selected($wear, $item['slug']); ?>>
|
||||
<?php echo esc_html($item['label']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e('仅可佩戴已授予徽章;用户也可在前台自行佩戴。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
wp_nonce_field('aksrt_user_badges_admin', 'aksrt_user_badges_nonce');
|
||||
}
|
||||
|
||||
public function handle_update(int $user_id): void {
|
||||
if (!$this->can_manage()) {
|
||||
return;
|
||||
}
|
||||
if (!isset($_POST['aksrt_user_badges_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_user_badges_nonce'])), 'aksrt_user_badges_admin')) {
|
||||
return;
|
||||
}
|
||||
if (!class_exists('AKSRT_User_Badges') || !AKSRT_User_Badges::enabled()) {
|
||||
return;
|
||||
}
|
||||
$raw = isset($_POST['aksrt_user_badges']) ? (array) wp_unslash($_POST['aksrt_user_badges']) : [];
|
||||
$slugs = array_map('sanitize_key', $raw);
|
||||
AKSRT_User_Badges::set_owned($user_id, $slugs);
|
||||
|
||||
$wear = isset($_POST['aksrt_wear_badge']) ? sanitize_key((string) wp_unslash($_POST['aksrt_wear_badge'])) : '';
|
||||
if ($wear === '') {
|
||||
delete_user_meta($user_id, AKSRT_User_Badges::META_WEAR);
|
||||
} else {
|
||||
$res = AKSRT_User_Badges::wear($user_id, $wear);
|
||||
if (is_wp_error($res)) {
|
||||
// leave owned as-is; wear ignored if invalid
|
||||
delete_user_meta($user_id, AKSRT_User_Badges::META_WEAR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
<?php
|
||||
/**
|
||||
* User badges — own / wear / grant (Zibll medal stack parity).
|
||||
*
|
||||
* Meta:
|
||||
* aksrt_user_badges string[]|CSV owned slug list
|
||||
* aksrt_wear_badge string currently worn slug
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_User_Badges {
|
||||
|
||||
public const META_OWNED = 'aksrt_user_badges';
|
||||
public const META_WEAR = 'aksrt_wear_badge';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('rest_api_init', [$this, 'register_routes']);
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
register_rest_route('aksrt/v1', '/users/(?P<id>\d+)/badges', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_list'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/users/me/badges/wear', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_wear'],
|
||||
'permission_callback' => static function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/users/(?P<id>\d+)/badges/grant', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_grant'],
|
||||
'permission_callback' => [$this, 'rest_can_manually_set'],
|
||||
'args' => [
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/users/(?P<id>\d+)/badges/revoke', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_revoke'],
|
||||
'permission_callback' => [$this, 'rest_can_manually_set'],
|
||||
'args' => [
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_can_manually_set(): bool {
|
||||
return self::can_manually_set(get_current_user_id());
|
||||
}
|
||||
|
||||
public static function enabled(): bool {
|
||||
if (!class_exists('AKSRT_Settings')) {
|
||||
return false;
|
||||
}
|
||||
$settings = AKSRT_Settings::get_user_identity_settings();
|
||||
return !empty($settings['badge']['enabled']);
|
||||
}
|
||||
|
||||
public static function public_display(): bool {
|
||||
if (!class_exists('AKSRT_Settings')) {
|
||||
return false;
|
||||
}
|
||||
$settings = AKSRT_Settings::get_user_identity_settings();
|
||||
return !empty($settings['badge']['enabled']) && !empty($settings['badge']['public_display']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{slug:string,label:string,tone:string,icon_url:string}>
|
||||
*/
|
||||
public static function get_dictionary(): array {
|
||||
if (!class_exists('AKSRT_Settings')) {
|
||||
return [];
|
||||
}
|
||||
$settings = AKSRT_Settings::get_user_identity_settings();
|
||||
$dict = is_array($settings['badge']['dictionary'] ?? null) ? $settings['badge']['dictionary'] : [];
|
||||
$out = [];
|
||||
foreach ($dict as $item) {
|
||||
$slug = sanitize_key((string) ($item['slug'] ?? ''));
|
||||
if ($slug === '') {
|
||||
continue;
|
||||
}
|
||||
$out[] = [
|
||||
'slug' => $slug,
|
||||
'label' => (string) ($item['label'] ?? $slug),
|
||||
'tone' => (string) ($item['tone'] ?? 'neutral'),
|
||||
'icon_url' => (string) ($item['icon_url'] ?? ''),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array{slug:string,label:string,tone:string,icon_url:string}>
|
||||
*/
|
||||
public static function get_dictionary_map(): array {
|
||||
$map = [];
|
||||
foreach (self::get_dictionary() as $item) {
|
||||
$map[$item['slug']] = $item;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function get_owned(int $user_id): array {
|
||||
if ($user_id <= 0) {
|
||||
return [];
|
||||
}
|
||||
$raw = get_user_meta($user_id, self::META_OWNED, true);
|
||||
if (!is_array($raw)) {
|
||||
$raw = preg_split('/[\s,]+/', (string) $raw, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
$map = self::get_dictionary_map();
|
||||
$out = [];
|
||||
foreach ((array) $raw as $slug) {
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$out[] = $slug;
|
||||
}
|
||||
return array_values(array_unique($out));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $slugs
|
||||
*/
|
||||
public static function set_owned(int $user_id, array $slugs): void {
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
$map = self::get_dictionary_map();
|
||||
$clean = [];
|
||||
foreach ($slugs as $slug) {
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug !== '' && isset($map[$slug])) {
|
||||
$clean[] = $slug;
|
||||
}
|
||||
}
|
||||
$clean = array_values(array_unique($clean));
|
||||
update_user_meta($user_id, self::META_OWNED, $clean);
|
||||
|
||||
$wear = self::get_wear($user_id);
|
||||
if ($wear !== '' && !in_array($wear, $clean, true)) {
|
||||
delete_user_meta($user_id, self::META_WEAR);
|
||||
}
|
||||
}
|
||||
|
||||
public static function get_wear(int $user_id): string {
|
||||
if ($user_id <= 0) {
|
||||
return '';
|
||||
}
|
||||
$wear = sanitize_key((string) get_user_meta($user_id, self::META_WEAR, true));
|
||||
if ($wear === '') {
|
||||
return '';
|
||||
}
|
||||
$owned = self::get_owned($user_id);
|
||||
return in_array($wear, $owned, true) ? $wear : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function wear(int $user_id, string $slug) {
|
||||
if ($user_id <= 0) {
|
||||
return new WP_Error('aksrt_badge_user', __('无效用户', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
if (!self::enabled()) {
|
||||
return new WP_Error('aksrt_badge_disabled', __('徽章系统未启用', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
$slug = sanitize_key($slug);
|
||||
$map = self::get_dictionary_map();
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
return new WP_Error('aksrt_badge_unknown', __('徽章不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$owned = self::get_owned($user_id);
|
||||
if (!in_array($slug, $owned, true)) {
|
||||
return new WP_Error('aksrt_badge_not_owned', __('尚未获得该徽章', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
update_user_meta($user_id, self::META_WEAR, $slug);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function grant(int $user_id, string $slug, int $actor = 0) {
|
||||
if ($user_id <= 0) {
|
||||
return new WP_Error('aksrt_badge_user', __('无效用户', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
if (!self::enabled()) {
|
||||
return new WP_Error('aksrt_badge_disabled', __('徽章系统未启用', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
$slug = sanitize_key($slug);
|
||||
$map = self::get_dictionary_map();
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
return new WP_Error('aksrt_badge_unknown', __('徽章不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$owned = self::get_owned($user_id);
|
||||
if (!in_array($slug, $owned, true)) {
|
||||
$owned[] = $slug;
|
||||
self::set_owned($user_id, $owned);
|
||||
}
|
||||
/**
|
||||
* Fires after a badge is granted.
|
||||
*
|
||||
* @param int $user_id
|
||||
* @param string $slug
|
||||
* @param int $actor
|
||||
*/
|
||||
do_action('aksrt_badge_granted', $user_id, $slug, $actor);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function revoke(int $user_id, string $slug, int $actor = 0) {
|
||||
if ($user_id <= 0) {
|
||||
return new WP_Error('aksrt_badge_user', __('无效用户', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$slug = sanitize_key($slug);
|
||||
$owned = self::get_owned($user_id);
|
||||
$owned = array_values(array_filter($owned, static function ($s) use ($slug) {
|
||||
return $s !== $slug;
|
||||
}));
|
||||
self::set_owned($user_id, $owned);
|
||||
/**
|
||||
* @param int $user_id
|
||||
* @param string $slug
|
||||
* @param int $actor
|
||||
*/
|
||||
do_action('aksrt_badge_revoked', $user_id, $slug, $actor);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function can_manually_set(int $actor = 0): bool {
|
||||
$actor = $actor > 0 ? $actor : get_current_user_id();
|
||||
if ($actor <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (user_can($actor, 'manage_options')) {
|
||||
return true;
|
||||
}
|
||||
if (class_exists('AKSRT_Settings_Capabilities')) {
|
||||
return AKSRT_Settings_Capabilities::user_can('medal_manually_set', $actor);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public DTO for REST / modals.
|
||||
*
|
||||
* @return array{enabled:bool,public_display:bool,wear:string,items:list<array>}
|
||||
*/
|
||||
public static function list_for_user(int $user_id, bool $include_private = false): array {
|
||||
$enabled = self::enabled();
|
||||
$public = self::public_display();
|
||||
$viewer = get_current_user_id();
|
||||
$can_see = $include_private
|
||||
|| $public
|
||||
|| ($viewer > 0 && $viewer === $user_id)
|
||||
|| self::can_manually_set($viewer);
|
||||
|
||||
if (!$enabled || !$can_see || $user_id <= 0) {
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'public_display' => $public,
|
||||
'wear' => '',
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$map = self::get_dictionary_map();
|
||||
$owned = self::get_owned($user_id);
|
||||
$wear = self::get_wear($user_id);
|
||||
// Prefer worn first (Zibll).
|
||||
if ($wear !== '') {
|
||||
$owned = array_values(array_unique(array_merge([$wear], $owned)));
|
||||
}
|
||||
$items = [];
|
||||
foreach ($owned as $slug) {
|
||||
if (!isset($map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'slug' => $slug,
|
||||
'label' => $map[$slug]['label'],
|
||||
'tone' => $map[$slug]['tone'],
|
||||
'icon_url' => $map[$slug]['icon_url'],
|
||||
'wear' => ($slug === $wear),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => true,
|
||||
'public_display' => $public,
|
||||
'wear' => $wear,
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
public function rest_list(WP_REST_Request $request) {
|
||||
$user_id = (int) $request['id'];
|
||||
if ($user_id <= 0 || !get_userdata($user_id)) {
|
||||
return new WP_Error('aksrt_badge_user', __('用户不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$data = self::list_for_user($user_id);
|
||||
return rest_ensure_response(['success' => true, 'data' => $data]);
|
||||
}
|
||||
|
||||
public function rest_wear(WP_REST_Request $request) {
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? $params['name'] ?? ''));
|
||||
$result = self::wear(get_current_user_id(), $slug);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => self::list_for_user(get_current_user_id(), true),
|
||||
'message' => __('已佩戴徽章', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_grant(WP_REST_Request $request) {
|
||||
$user_id = (int) $request['id'];
|
||||
if ($user_id <= 0 || !get_userdata($user_id)) {
|
||||
return new WP_Error('aksrt_badge_user', __('用户不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? $params['name'] ?? ''));
|
||||
$result = self::grant($user_id, $slug, get_current_user_id());
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => self::list_for_user($user_id, true),
|
||||
'message' => __('已授予徽章', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_revoke(WP_REST_Request $request) {
|
||||
$user_id = (int) $request['id'];
|
||||
if ($user_id <= 0 || !get_userdata($user_id)) {
|
||||
return new WP_Error('aksrt_badge_user', __('用户不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? $params['name'] ?? ''));
|
||||
$result = self::revoke($user_id, $slug, get_current_user_id());
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => self::list_for_user($user_id, true),
|
||||
'message' => __('已收回徽章', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Auto-grant medals whose get_type metrics meet get_val (skip manually_add).
|
||||
*/
|
||||
public static function auto_evaluate(int $user_id): void {
|
||||
if ($user_id <= 0 || !self::enabled() || !class_exists('AKSRT_Settings')) {
|
||||
return;
|
||||
}
|
||||
$settings = AKSRT_Settings::get_user_identity_settings();
|
||||
$medal = is_array($settings['medal'] ?? null) ? $settings['medal'] : [];
|
||||
$items = is_array($medal['items_by_slug'] ?? null) ? $medal['items_by_slug'] : [];
|
||||
if (!$items) {
|
||||
// Fall back to dictionary only — no get_type → nothing to auto-grant.
|
||||
return;
|
||||
}
|
||||
$owned = self::get_owned($user_id);
|
||||
$owned_map = array_fill_keys($owned, true);
|
||||
foreach ($items as $slug => $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug === '' || !empty($owned_map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$type = sanitize_key((string) ($item['get_type'] ?? 'manually_add'));
|
||||
if ($type === '' || $type === 'manually_add') {
|
||||
continue;
|
||||
}
|
||||
$need = max(0, (float) ($item['get_val'] ?? 0));
|
||||
$have = (float) self::metric_value($user_id, $type);
|
||||
if ($have >= $need) {
|
||||
self::grant($user_id, $slug, 0);
|
||||
$owned_map[$slug] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Numeric metric for a medal get_type.
|
||||
*/
|
||||
public static function metric_value(int $user_id, string $type): float {
|
||||
switch ($type) {
|
||||
case 'registration_time':
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return 0.0;
|
||||
}
|
||||
$reg = get_date_from_gmt($user->user_registered, 'Y-m-d H:i:s');
|
||||
$now = current_time('Y-m-d H:i:s');
|
||||
return max(0.0, (strtotime($now) - strtotime($reg)) / 86400);
|
||||
|
||||
case 'checkin_all_day':
|
||||
// Prefer dedicated check-in total meta if present.
|
||||
$v = get_user_meta($user_id, 'aksrt_checkin_all_day', true);
|
||||
if ($v === '' || $v === false) {
|
||||
$v = get_user_meta($user_id, 'checkin_all_day', true);
|
||||
}
|
||||
return max(0.0, (float) $v);
|
||||
|
||||
case 'new_post':
|
||||
return (float) count_user_posts($user_id, 'post', true);
|
||||
|
||||
case 'bbs_post':
|
||||
$types = post_type_exists('forum_topic') ? 'forum_topic' : (post_type_exists('topic') ? 'topic' : '');
|
||||
return $types !== '' ? (float) count_user_posts($user_id, $types, true) : 0.0;
|
||||
|
||||
case 'comment':
|
||||
$counts = get_comments(['user_id' => $user_id, 'count' => true, 'status' => 'approve']);
|
||||
return (float) $counts;
|
||||
|
||||
case 'followed':
|
||||
if (class_exists('AKSRT_Follow')) {
|
||||
return (float) AKSRT_Follow::get_instance()->get_follower_count($user_id);
|
||||
}
|
||||
return 0.0;
|
||||
|
||||
case 'like':
|
||||
return (float) get_user_meta($user_id, 'aksrt_posts_like_sum', true);
|
||||
|
||||
case 'comment_like':
|
||||
return (float) get_user_meta($user_id, 'aksrt_comment_like_sum', true);
|
||||
|
||||
case 'bbs_adopt':
|
||||
return (float) get_user_meta($user_id, 'aksrt_bbs_adopt_count', true);
|
||||
|
||||
case 'bbs_comment_hot':
|
||||
return (float) get_user_meta($user_id, 'aksrt_bbs_comment_hot_count', true);
|
||||
|
||||
case 'bbs_post_hot':
|
||||
return (float) get_user_meta($user_id, 'aksrt_bbs_post_hot_count', true);
|
||||
|
||||
case 'bbs_essence':
|
||||
return (float) get_user_meta($user_id, 'aksrt_bbs_essence_count', true);
|
||||
|
||||
case 'bbs_score':
|
||||
return (float) get_user_meta($user_id, 'aksrt_bbs_score_sum', true);
|
||||
|
||||
case 'views':
|
||||
$sum = (float) get_user_meta($user_id, 'aksrt_posts_views_sum', true);
|
||||
if ($sum > 0) {
|
||||
return $sum;
|
||||
}
|
||||
// Lazy backfill once from postmeta.
|
||||
global $wpdb;
|
||||
$sum = (float) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COALESCE(SUM(CAST(pm.meta_value AS UNSIGNED)),0)
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = 'aksrt_post_views'
|
||||
WHERE p.post_author = %d AND p.post_type = 'post' AND p.post_status = 'publish'",
|
||||
$user_id
|
||||
));
|
||||
if ($sum > 0) {
|
||||
update_user_meta($user_id, 'aksrt_posts_views_sum', $sum);
|
||||
}
|
||||
return $sum;
|
||||
|
||||
case 'pay_income':
|
||||
return (float) get_user_meta($user_id, 'aksrt_pay_income_sum', true);
|
||||
|
||||
case 'pay_rebate':
|
||||
return (float) get_user_meta($user_id, 'aksrt_pay_rebate_sum', true);
|
||||
|
||||
default:
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $user_login
|
||||
* @param WP_User $user
|
||||
*/
|
||||
public function on_login($user_login, $user): void {
|
||||
$user_id = $user instanceof WP_User ? (int) $user->ID : 0;
|
||||
if ($user_id > 0) {
|
||||
self::auto_evaluate($user_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_current_user_probe(): void {
|
||||
$uid = get_current_user_id();
|
||||
if ($uid > 0) {
|
||||
self::auto_evaluate($uid);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin cash wallet adjust on WP user screens.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_User_Balance_Admin {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_filter('manage_users_columns', [$this, 'add_column']);
|
||||
add_filter('manage_users_custom_column', [$this, 'render_column'], 10, 3);
|
||||
add_filter('manage_users_sortable_columns', [$this, 'sortable']);
|
||||
add_action('pre_get_users', [$this, 'sort_users']);
|
||||
add_action('show_user_profile', [$this, 'render_section']);
|
||||
add_action('edit_user_profile', [$this, 'render_section']);
|
||||
add_action('personal_options_update', [$this, 'handle_update']);
|
||||
add_action('edit_user_profile_update', [$this, 'handle_update']);
|
||||
}
|
||||
|
||||
private function can_manage(): bool {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
|
||||
public function add_column(array $columns): array {
|
||||
if (!class_exists('AKSRT_Balance') || !AKSRT_Balance::enabled()) {
|
||||
return $columns;
|
||||
}
|
||||
$columns['aksrt_wallet_balance'] = __('现金余额', 'aksrt');
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function render_column(string $output, string $column_name, int $user_id): string {
|
||||
if ($column_name !== 'aksrt_wallet_balance') {
|
||||
return $output;
|
||||
}
|
||||
if (!class_exists('AKSRT_Balance')) {
|
||||
return '—';
|
||||
}
|
||||
return '<strong>' . esc_html(AKSRT_Balance::format(AKSRT_Balance::balance($user_id))) . '</strong>';
|
||||
}
|
||||
|
||||
public function sortable(array $columns): array {
|
||||
$columns['aksrt_wallet_balance'] = 'aksrt_wallet_balance';
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function sort_users($query): void {
|
||||
if (!is_admin() || $query->get('orderby') !== 'aksrt_wallet_balance') {
|
||||
return;
|
||||
}
|
||||
$query->set('meta_key', 'aksrt_balance');
|
||||
$query->set('orderby', 'meta_value_num');
|
||||
}
|
||||
|
||||
public function render_section(WP_User $user): void {
|
||||
if (!class_exists('AKSRT_Balance') || !AKSRT_Balance::enabled()) {
|
||||
return;
|
||||
}
|
||||
$balance = AKSRT_Balance::balance((int) $user->ID);
|
||||
$can = $this->can_manage();
|
||||
$log = AKSRT_Balance::get_log(['user_id' => (int) $user->ID, 'page' => 1, 'per_page' => 8]);
|
||||
?>
|
||||
<a id="aksrt-user-wallet"></a>
|
||||
<h2><?php esc_html_e('现金钱包(AKSRT)', 'aksrt'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th><?php esc_html_e('当前余额', 'aksrt'); ?></th>
|
||||
<td><strong style="font-size:18px;"><?php echo esc_html(AKSRT_Balance::format($balance)); ?></strong> <?php esc_html_e('元', 'aksrt'); ?></td>
|
||||
</tr>
|
||||
<?php if ($can) : ?>
|
||||
<tr>
|
||||
<th><?php esc_html_e('调整余额', 'aksrt'); ?></th>
|
||||
<td>
|
||||
<?php wp_nonce_field('aksrt_user_balance_adjust_' . (int) $user->ID, '_aksrt_balance_nonce'); ?>
|
||||
<label for="aksrt_balance_delta"><?php esc_html_e('变动金额(正数=增加,负数=扣减)', 'aksrt'); ?></label><br>
|
||||
<input type="number" step="0.01" name="aksrt_balance_delta" id="aksrt_balance_delta" value="" class="regular-text" />
|
||||
<p>
|
||||
<label for="aksrt_balance_reason"><?php esc_html_e('备注', 'aksrt'); ?></label><br>
|
||||
<input type="text" name="aksrt_balance_reason" id="aksrt_balance_reason" class="regular-text" />
|
||||
</p>
|
||||
<p class="description"><?php esc_html_e('保存用户资料时生效,并写入余额流水。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr>
|
||||
<th><?php esc_html_e('近期流水', 'aksrt'); ?></th>
|
||||
<td>
|
||||
<?php if (!empty($log['items'])) : ?>
|
||||
<table class="widefat striped" style="max-width:640px;">
|
||||
<thead><tr><th><?php esc_html_e('时间', 'aksrt'); ?></th><th><?php esc_html_e('变动', 'aksrt'); ?></th><th><?php esc_html_e('余额', 'aksrt'); ?></th><th><?php esc_html_e('原因', 'aksrt'); ?></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($log['items'] as $row) : ?>
|
||||
<tr>
|
||||
<td><?php echo esc_html(get_date_from_gmt((string) ($row['created_at'] ?? ''), 'Y-m-d H:i')); ?></td>
|
||||
<td><?php echo esc_html(($row['delta'] > 0 ? '+' : '') . AKSRT_Balance::format((float) $row['delta'])); ?></td>
|
||||
<td><?php echo esc_html(AKSRT_Balance::format((float) ($row['balance_after'] ?? 0))); ?></td>
|
||||
<td><?php echo esc_html(AKSRT_Balance::reason_label((string) ($row['reason'] ?? ''))); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else : ?>
|
||||
<p><?php esc_html_e('暂无流水', 'aksrt'); ?></p>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function handle_update(int $user_id): void {
|
||||
if (!$this->can_manage() || !class_exists('AKSRT_Balance') || !AKSRT_Balance::enabled()) {
|
||||
return;
|
||||
}
|
||||
if (empty($_POST['_aksrt_balance_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['_aksrt_balance_nonce'])), 'aksrt_user_balance_adjust_' . $user_id)) {
|
||||
return;
|
||||
}
|
||||
if (!isset($_POST['aksrt_balance_delta']) || $_POST['aksrt_balance_delta'] === '') {
|
||||
return;
|
||||
}
|
||||
$delta = round((float) wp_unslash($_POST['aksrt_balance_delta']), 2);
|
||||
if (abs($delta) < 0.0001) {
|
||||
return;
|
||||
}
|
||||
$reason = isset($_POST['aksrt_balance_reason'])
|
||||
? sanitize_text_field(wp_unslash((string) $_POST['aksrt_balance_reason']))
|
||||
: __('管理员调整', 'aksrt');
|
||||
if ($delta > 0) {
|
||||
$result = AKSRT_Balance::add($user_id, $delta, 'admin', ['type' => 'user', 'id' => get_current_user_id()], $reason);
|
||||
} else {
|
||||
$result = AKSRT_Balance::deduct($user_id, abs($delta), 'admin', ['type' => 'user', 'id' => get_current_user_id()], $reason);
|
||||
}
|
||||
if (is_wp_error($result)) {
|
||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [[
|
||||
'type' => 'error',
|
||||
'msg' => $result->get_error_message(),
|
||||
]], 30);
|
||||
return;
|
||||
}
|
||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [[
|
||||
'type' => 'success',
|
||||
'msg' => __('现金余额已更新', 'aksrt'),
|
||||
]], 30);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT User Center —本人中心协调类
|
||||
*
|
||||
* 负责本人中心页面的初始化、面板注册和资源加载协调。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_User_Center {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
/** @var array 面板注册表 */
|
||||
private $panels = [];
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
$this->register_default_panels();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册默认面板
|
||||
*/
|
||||
private function register_default_panels(): void {
|
||||
$settings = class_exists('AKSRT_Settings')
|
||||
? AKSRT_Settings::get_user_center_settings()
|
||||
: [
|
||||
'tabs' => ['profile', 'posts', 'comments', 'favorites'],
|
||||
'avatar_upload' => true,
|
||||
'cover_upload' => true,
|
||||
'social_links' => true,
|
||||
];
|
||||
|
||||
$definitions = [
|
||||
'profile' => [
|
||||
'label' => __('个人信息', 'aksrt'),
|
||||
'icon' => 'user',
|
||||
'priority' => 10,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'posts' => [
|
||||
'label' => __('我的文章', 'aksrt'),
|
||||
'icon' => 'file-alt',
|
||||
'priority' => 20,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'comments' => [
|
||||
'label' => __('我的评论', 'aksrt'),
|
||||
'icon' => 'comment',
|
||||
'priority' => 30,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'favorites' => [
|
||||
'label' => __('我的收藏', 'aksrt'),
|
||||
'icon' => 'star',
|
||||
'priority' => 40,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'credits' => [
|
||||
'label' => class_exists('AKSRT_Settings')
|
||||
? sprintf(__('我的%s', 'aksrt'), AKSRT_Settings::get_currency_name())
|
||||
: __('我的积分', 'aksrt'),
|
||||
'icon' => 'wallet',
|
||||
'priority' => 50,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'notif' => [
|
||||
'label' => __('消息通知', 'aksrt'),
|
||||
'icon' => 'bell',
|
||||
'priority' => 60,
|
||||
'type' => 'link',
|
||||
],
|
||||
'settings' => [
|
||||
'label' => __('账号设置', 'aksrt'),
|
||||
'icon' => 'cog',
|
||||
'priority' => 70,
|
||||
'type' => 'panel',
|
||||
],
|
||||
];
|
||||
|
||||
$this->panels = [];
|
||||
|
||||
foreach ((array) ($settings['tabs'] ?? []) as $key) {
|
||||
if ($key === 'credits') {
|
||||
$credits_on = class_exists('AKSRT_Settings') && AKSRT_Settings::credits_enabled();
|
||||
$balance_on = class_exists('AKSRT_Balance') && AKSRT_Balance::enabled();
|
||||
if (!$credits_on && !$balance_on) {
|
||||
continue;
|
||||
}
|
||||
if ($balance_on) {
|
||||
$definitions['credits']['label'] = __('我的资产', 'aksrt');
|
||||
$definitions['credits']['icon'] = 'wallet';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($definitions[$key])) {
|
||||
$this->panels[$key] = $definitions[$key];
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure assets panel when wallet and/or credits enabled (even if tabs list omitted credits).
|
||||
$credits_on = class_exists('AKSRT_Settings') && AKSRT_Settings::credits_enabled();
|
||||
$balance_on = class_exists('AKSRT_Balance') && AKSRT_Balance::enabled();
|
||||
if (!isset($this->panels['credits']) && ($credits_on || $balance_on)) {
|
||||
$panel = $definitions['credits'];
|
||||
if ($balance_on) {
|
||||
$panel['label'] = __('我的资产', 'aksrt');
|
||||
$panel['icon'] = 'wallet';
|
||||
}
|
||||
$this->panels['credits'] = $panel;
|
||||
}
|
||||
|
||||
if (!isset($this->panels['settings'])) {
|
||||
$this->panels['settings'] = $definitions['settings'];
|
||||
}
|
||||
|
||||
$this->panels['notif'] = $definitions['notif'];
|
||||
|
||||
/**
|
||||
* 允许插件/子主题注册额外面板
|
||||
*
|
||||
* @param array $panels 面板注册表
|
||||
*/
|
||||
$this->panels = apply_filters('aksrt_user_center_panels', $this->panels);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取面板注册表
|
||||
*/
|
||||
public function get_panels(): array {
|
||||
return $this->panels;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可切换的本地面板(不含外链入口)。
|
||||
*/
|
||||
public function get_dashboard_tabs(): array {
|
||||
return array_filter($this->panels, static function (array $panel): bool {
|
||||
return ($panel['type'] ?? 'panel') === 'panel';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前页面是否为本人中心
|
||||
*/
|
||||
public static function is_page(): bool {
|
||||
return function_exists('aksrt_is_user_center_page') && aksrt_is_user_center_page();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT User Credits Admin
|
||||
*
|
||||
* 把"用户积分管理"迁移到 WordPress 原生"用户"菜单:
|
||||
* - users.php:增加积分余额列(可排序)+ 行操作
|
||||
* - user-edit.php / profile.php:增加积分管理区块(管理员可调增减)+ 近期流水
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_User_Credits_Admin {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_filter('manage_users_columns', [$this, 'add_balance_column']);
|
||||
add_filter('manage_users_custom_column', [$this, 'render_balance_column'], 10, 3);
|
||||
add_filter('manage_users_sortable_columns', [$this, 'make_balance_sortable']);
|
||||
add_action('pre_get_users', [$this, 'sort_users_by_credits']);
|
||||
add_filter('user_row_actions', [$this, 'add_row_actions'], 30, 2);
|
||||
|
||||
add_action('show_user_profile', [$this, 'render_user_credits_section']);
|
||||
add_action('edit_user_profile', [$this, 'render_user_credits_section']);
|
||||
add_action('personal_options_update', [$this, 'handle_user_credits_update']);
|
||||
add_action('edit_user_profile_update', [$this, 'handle_user_credits_update']);
|
||||
}
|
||||
|
||||
private function can_manage_credits(): bool {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
|
||||
// ========== 用户列表:积分余额列 ==========
|
||||
|
||||
public function add_balance_column(array $columns): array {
|
||||
$currency_name = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
$columns['aksrt_credits_balance'] = sprintf(__('%s余额', 'aksrt'), $currency_name);
|
||||
$columns['aksrt_user_commerce'] = __('资产/分成', 'aksrt');
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function render_balance_column(string $output, string $column_name, int $user_id): string {
|
||||
if ($column_name === 'aksrt_user_commerce') {
|
||||
return $this->render_commerce_column($user_id);
|
||||
}
|
||||
if ($column_name !== 'aksrt_credits_balance') {
|
||||
return $output;
|
||||
}
|
||||
if (!class_exists('AKSRT_Credits')) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
$balance = AKSRT_Credits::balance($user_id);
|
||||
return '<strong>' . esc_html((string) $balance) . '</strong>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact commerce snapshot: VIP, revenue, rebate, download logs link.
|
||||
*/
|
||||
private function render_commerce_column(int $user_id): string {
|
||||
$bits = [];
|
||||
if (function_exists('aksrt_get_user_vip_level')) {
|
||||
$lv = (int) aksrt_get_user_vip_level($user_id);
|
||||
if ($lv > 0) {
|
||||
$bits[] = '<span class="aksrt-vip-badge">VIP' . esc_html((string) $lv) . '</span>';
|
||||
}
|
||||
}
|
||||
if (class_exists('AKSRT_Revenue')) {
|
||||
$stats = AKSRT_Revenue::get_instance()->get_stats($user_id);
|
||||
$pending = (float) ($stats['pending'] ?? 0);
|
||||
$total = (float) ($stats['total'] ?? 0);
|
||||
if ($total > 0 || $pending > 0) {
|
||||
$bits[] = sprintf(
|
||||
/* translators: 1 total 2 pending */
|
||||
esc_html__('分成 ¥%1$s(待结 ¥%2$s)', 'aksrt'),
|
||||
number_format($total, 2),
|
||||
number_format($pending, 2)
|
||||
);
|
||||
$bits[] = '<a href="' . esc_url(admin_url('admin.php?page=aksrt_income&user_id=' . $user_id)) . '">' . esc_html__('分成明细', 'aksrt') . '</a>';
|
||||
}
|
||||
}
|
||||
if (class_exists('AKSRT_Rebate')) {
|
||||
$list = AKSRT_Rebate::get_instance()->get_list(['user_id' => $user_id, 'limit' => 1]);
|
||||
$total_r = (int) ($list['total'] ?? 0);
|
||||
if ($total_r > 0) {
|
||||
$bits[] = sprintf(esc_html__('返佣 %d 笔', 'aksrt'), $total_r);
|
||||
$bits[] = '<a href="' . esc_url(admin_url('admin.php?page=aksrt_rebate&user_id=' . $user_id)) . '">' . esc_html__('返佣', 'aksrt') . '</a>';
|
||||
}
|
||||
}
|
||||
$bits[] = '<a href="' . esc_url(admin_url('admin.php?page=aksrt_download_logs&user_id=' . $user_id)) . '">' . esc_html__('下载记录', 'aksrt') . '</a>';
|
||||
$bits[] = '<a href="' . esc_url(admin_url('admin.php?page=aksrt_orders&user_id=' . $user_id)) . '">' . esc_html__('订单', 'aksrt') . '</a>';
|
||||
|
||||
if (!$bits) {
|
||||
return '—';
|
||||
}
|
||||
return '<div style="font-size:12px;line-height:1.5;">' . implode('<br>', $bits) . '</div>';
|
||||
}
|
||||
|
||||
public function make_balance_sortable(array $columns): array {
|
||||
$columns['aksrt_credits_balance'] = 'aksrt_credits_balance';
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function sort_users_by_credits($query): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
if ($query->get('orderby') !== 'aksrt_credits_balance') {
|
||||
return;
|
||||
}
|
||||
$query->set('meta_key', 'aksrt_credits');
|
||||
$query->set('orderby', 'meta_value_num');
|
||||
}
|
||||
|
||||
// ========== 用户列表:行操作 ==========
|
||||
|
||||
public function add_row_actions(array $actions, WP_User $user): array {
|
||||
if (!$this->can_manage_credits()) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$edit_url = get_edit_user_link($user->ID);
|
||||
if ($edit_url) {
|
||||
$currency_name = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
$actions['aksrt_edit_credits'] = sprintf(
|
||||
'<a href="%s#aksrt-user-credits">%s</a>',
|
||||
esc_url($edit_url),
|
||||
esc_html(sprintf(__('调整%s', 'aksrt'), $currency_name))
|
||||
);
|
||||
}
|
||||
$actions['aksrt_user_dl_logs'] = sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
esc_url(admin_url('admin.php?page=aksrt_download_logs&user_id=' . (int) $user->ID)),
|
||||
esc_html__('下载记录', 'aksrt')
|
||||
);
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
// ========== 用户编辑页:积分管理区块 ==========
|
||||
|
||||
public function render_user_credits_section(WP_User $user): void {
|
||||
if (!class_exists('AKSRT_Credits')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$balance = AKSRT_Credits::balance((int) $user->ID);
|
||||
$can_manage = $this->can_manage_credits();
|
||||
$currency_name = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
?>
|
||||
<a id="aksrt-user-credits"></a>
|
||||
<h2><?php echo esc_html(sprintf(__('%s管理(AKSRT)', 'aksrt'), $currency_name)); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th><?php esc_html_e('当前余额', 'aksrt'); ?></th>
|
||||
<td>
|
||||
<strong style="font-size:18px;"><?php echo esc_html((string) $balance); ?></strong>
|
||||
<span style="color:#666;margin-left:6px;"><?php echo esc_html($currency_name); ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php if ($can_manage) : ?>
|
||||
<tr>
|
||||
<th><?php echo esc_html(sprintf(__('调整%s', 'aksrt'), $currency_name)); ?></th>
|
||||
<td>
|
||||
<?php wp_nonce_field('aksrt_user_credits_adjust_' . (int) $user->ID, '_aksrt_credits_nonce'); ?>
|
||||
<fieldset>
|
||||
<label for="aksrt_credits_delta" style="display:block;margin-bottom:6px;">
|
||||
<?php esc_html_e('变动数量(正数=增加,负数=扣减)', 'aksrt'); ?>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="aksrt_credits_delta"
|
||||
name="aksrt_credits_delta"
|
||||
class="regular-text"
|
||||
min="-100000"
|
||||
max="100000"
|
||||
step="1"
|
||||
value="0"
|
||||
>
|
||||
<p class="description" style="margin-bottom:12px;">
|
||||
<?php esc_html_e('填写 0 不会执行任何变更。', 'aksrt'); ?>
|
||||
</p>
|
||||
|
||||
<label for="aksrt_credits_reason" style="display:block;margin-bottom:6px;">
|
||||
<?php esc_html_e('调整原因', 'aksrt'); ?>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="aksrt_credits_reason"
|
||||
name="aksrt_credits_reason"
|
||||
class="regular-text"
|
||||
maxlength="100"
|
||||
placeholder="<?php echo esc_attr__('例如:手工补偿 / 运营赠送 / 退款修正', 'aksrt'); ?>"
|
||||
>
|
||||
<p class="description">
|
||||
<?php esc_html_e('勾选后点击页面底部"更新用户"保存即可生效。', 'aksrt'); ?>
|
||||
</p>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
|
||||
</table>
|
||||
|
||||
<?php
|
||||
// ========== 近期积分流水 ==========
|
||||
$log = AKSRT_Credits::get_log(['user_id' => (int) $user->ID, 'page' => 1, 'per_page' => 20]);
|
||||
$items = $log['items'] ?? [];
|
||||
$reason_labels = [
|
||||
'register' => __('注册赠送', 'aksrt'),
|
||||
'signin' => __('每日签到', 'aksrt'),
|
||||
'comment' => __('评论奖励', 'aksrt'),
|
||||
'invite' => __('邀请奖励', 'aksrt'),
|
||||
'invitee' => __('被邀请奖励', 'aksrt'),
|
||||
'download' => __('下载消费', 'aksrt'),
|
||||
'admin' => __('管理员调整', 'aksrt'),
|
||||
];
|
||||
?>
|
||||
<h3 style="margin-top:24px;"><?php echo esc_html(sprintf(__('近期%s流水', 'aksrt'), $currency_name)); ?></h3>
|
||||
<?php if (!empty($items)) : ?>
|
||||
<table class="widefat striped" style="max-width:800px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e('时间', 'aksrt'); ?></th>
|
||||
<th><?php esc_html_e('类型', 'aksrt'); ?></th>
|
||||
<th><?php esc_html_e('备注', 'aksrt'); ?></th>
|
||||
<th><?php esc_html_e('变动', 'aksrt'); ?></th>
|
||||
<th><?php esc_html_e('余额', 'aksrt'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($items as $row) :
|
||||
$delta = (int) $row['delta'];
|
||||
$reason_key = (string) ($row['reason'] ?? '');
|
||||
$label = $reason_labels[$reason_key] ?? $reason_key;
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo esc_html(get_date_from_gmt((string) $row['created_at'], 'Y-m-d H:i')); ?></td>
|
||||
<td><?php echo esc_html($label); ?></td>
|
||||
<td><?php echo esc_html((string) ($row['remark'] ?? '')); ?></td>
|
||||
<td style="color:<?php echo $delta >= 0 ? '#16a34a' : '#ef4444'; ?>; font-weight:600;">
|
||||
<?php echo esc_html(($delta > 0 ? '+' : '') . $delta); ?>
|
||||
</td>
|
||||
<td><?php echo esc_html((string) (int) $row['balance_after']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else : ?>
|
||||
<p style="color:#888;"><?php echo esc_html(sprintf(__('暂无%s流水记录。', 'aksrt'), $currency_name)); ?></p>
|
||||
<?php endif;
|
||||
}
|
||||
|
||||
// ========== 处理积分调整 ==========
|
||||
|
||||
public function handle_user_credits_update(int $user_id): void {
|
||||
if (!$this->can_manage_credits() || !class_exists('AKSRT_Credits')) {
|
||||
return;
|
||||
}
|
||||
if (!current_user_can('edit_user', $user_id)) {
|
||||
return;
|
||||
}
|
||||
if (!isset($_POST['_aksrt_credits_nonce'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nonce = sanitize_text_field((string) $_POST['_aksrt_credits_nonce']);
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_user_credits_adjust_' . $user_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$delta = isset($_POST['aksrt_credits_delta']) ? (int) $_POST['aksrt_credits_delta'] : 0;
|
||||
$reason = isset($_POST['aksrt_credits_reason']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_credits_reason'])) : '';
|
||||
|
||||
if ($delta === 0) {
|
||||
return;
|
||||
}
|
||||
$currency_name = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
if ($reason === '') {
|
||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [[
|
||||
'type' => 'error',
|
||||
'msg' => sprintf(__('调整%s时请填写原因。', 'aksrt'), $currency_name),
|
||||
]], 30);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($delta > 0) {
|
||||
$result = AKSRT_Credits::add($user_id, $delta, 'admin', ['type' => 'user', 'id' => get_current_user_id()], $reason);
|
||||
} else {
|
||||
$result = AKSRT_Credits::deduct($user_id, abs($delta), 'admin', ['type' => 'user', 'id' => get_current_user_id()], $reason);
|
||||
}
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [[
|
||||
'type' => 'error',
|
||||
'msg' => $result->get_error_message(),
|
||||
]], 30);
|
||||
return;
|
||||
}
|
||||
|
||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [[
|
||||
'type' => 'success',
|
||||
'msg' => sprintf(__('%s调整成功,当前余额:%d', 'aksrt'), $currency_name, (int) $result),
|
||||
]], 30);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT User Home — 公开用户主页协调类
|
||||
*
|
||||
* 负责公开用户主页(/user/{nicename}/)的初始化和 tab 注册。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_User_Home {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
/** @var array Tab 注册表 */
|
||||
private $tabs = [];
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
$this->register_default_tabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册默认公开 Tab
|
||||
*/
|
||||
private function register_default_tabs(): void {
|
||||
$this->tabs = [
|
||||
'posts' => [
|
||||
'label' => __('文章', 'aksrt'),
|
||||
'icon' => 'file-alt',
|
||||
'priority' => 10,
|
||||
],
|
||||
'comments' => [
|
||||
'label' => __('评论', 'aksrt'),
|
||||
'icon' => 'comment',
|
||||
'priority' => 20,
|
||||
],
|
||||
'favorites' => [
|
||||
'label' => __('收藏', 'aksrt'),
|
||||
'icon' => 'star',
|
||||
'priority' => 30,
|
||||
],
|
||||
'credits' => [
|
||||
'label' => class_exists('AKSRT_Settings')
|
||||
? sprintf(__('我的%s', 'aksrt'), AKSRT_Settings::get_currency_name())
|
||||
: __('我的积分', 'aksrt'),
|
||||
'icon' => 'wallet',
|
||||
'priority' => 40,
|
||||
],
|
||||
'product' => [
|
||||
'label' => __('商品', 'aksrt'),
|
||||
'icon' => 'shopping-bag',
|
||||
'priority' => 50,
|
||||
],
|
||||
'plate' => [
|
||||
'label' => __('圈子', 'aksrt'),
|
||||
'icon' => 'users',
|
||||
'priority' => 60,
|
||||
],
|
||||
'forum' => [
|
||||
'label' => __('论坛', 'aksrt'),
|
||||
'icon' => 'comments',
|
||||
'priority' => 70,
|
||||
],
|
||||
'follow' => [
|
||||
'label' => __('粉丝', 'aksrt'),
|
||||
'icon' => 'user-plus',
|
||||
'priority' => 80,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 允许插件/子主题注册额外公开 Tab
|
||||
*
|
||||
* @param array $tabs Tab 注册表
|
||||
*/
|
||||
$this->tabs = apply_filters('aksrt_user_home_tabs', $this->tabs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Tab 注册表
|
||||
*/
|
||||
public function get_tabs(): array {
|
||||
return $this->tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户主页实际可见的公开 Tab。
|
||||
*/
|
||||
public function get_visible_tabs(bool $is_own_profile = false): array {
|
||||
$settings = class_exists('AKSRT_Settings')
|
||||
? AKSRT_Settings::get_user_home_settings()
|
||||
: [
|
||||
'tabs' => ['posts', 'comments', 'favorites'],
|
||||
];
|
||||
|
||||
$visible = [];
|
||||
foreach ((array) ($settings['tabs'] ?? []) as $key) {
|
||||
if (!isset($this->tabs[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($key === 'credits' && (!$is_own_profile || !class_exists('AKSRT_Settings') || !AKSRT_Settings::credits_enabled())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$visible[$key] = $this->tabs[$key];
|
||||
}
|
||||
|
||||
if (empty($visible)) {
|
||||
foreach (['posts', 'comments', 'favorites'] as $fallback) {
|
||||
if (isset($this->tabs[$fallback])) {
|
||||
$visible[$fallback] = $this->tabs[$fallback];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公开展示的身份信号(认证 / 等级 / 徽章)。
|
||||
*
|
||||
* @return array<int,array{label:string,icon:string,tone:string,icon_url?:string,type?:string,slug?:string,wear?:bool}>
|
||||
*/
|
||||
public function get_profile_signals(int $user_id): array {
|
||||
if ($user_id <= 0 || !class_exists('AKSRT_Settings')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$settings = AKSRT_Settings::get_user_identity_settings();
|
||||
$signals = [];
|
||||
|
||||
if (!empty($settings['verify']['enabled']) && !empty($settings['verify']['public_display']) && $this->is_user_verified($user_id, $settings['verify'])) {
|
||||
$signals[] = [
|
||||
'type' => 'verify',
|
||||
'label' => (string) $settings['verify']['badge_text'],
|
||||
'icon' => 'check-circle',
|
||||
'tone' => 'success',
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($settings['level']['enabled']) && !empty($settings['level']['public_display'])) {
|
||||
$level = $this->get_user_level($user_id, $settings['level']);
|
||||
if ($level > 0) {
|
||||
$signals[] = [
|
||||
'type' => 'level',
|
||||
'label' => (string) $settings['level']['label_prefix'] . $level,
|
||||
'icon' => 'layer-group',
|
||||
'tone' => 'info',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($settings['badge']['enabled']) && !empty($settings['badge']['public_display'])) {
|
||||
$limit = max(1, (int) ($settings['badge']['limit'] ?? 3));
|
||||
if (class_exists('AKSRT_User_Badges')) {
|
||||
$list = AKSRT_User_Badges::list_for_user($user_id, true);
|
||||
$badge_count = 0;
|
||||
foreach ((array) ($list['items'] ?? []) as $item) {
|
||||
if ($badge_count >= $limit) {
|
||||
break;
|
||||
}
|
||||
$signals[] = [
|
||||
'type' => 'badge',
|
||||
'slug' => (string) ($item['slug'] ?? ''),
|
||||
'label' => (string) ($item['label'] ?? ''),
|
||||
'icon' => 'award',
|
||||
'tone' => (string) ($item['tone'] ?? 'neutral'),
|
||||
'icon_url' => (string) ($item['icon_url'] ?? ''),
|
||||
'wear' => !empty($item['wear']),
|
||||
];
|
||||
$badge_count++;
|
||||
}
|
||||
} else {
|
||||
$badge_map = [];
|
||||
foreach ((array) ($settings['badge']['dictionary'] ?? []) as $item) {
|
||||
if (!empty($item['slug'])) {
|
||||
$badge_map[(string) $item['slug']] = $item;
|
||||
}
|
||||
}
|
||||
$assigned = get_user_meta($user_id, 'aksrt_user_badges', true);
|
||||
if (!is_array($assigned)) {
|
||||
$assigned = preg_split('/[\s,]+/', (string) $assigned, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
$wear = sanitize_key((string) get_user_meta($user_id, 'aksrt_wear_badge', true));
|
||||
$assigned = array_values(array_filter(array_map(static function ($slug) {
|
||||
return sanitize_key((string) $slug);
|
||||
}, (array) $assigned)));
|
||||
if ($wear !== '' && in_array($wear, $assigned, true) && isset($badge_map[$wear])) {
|
||||
$assigned = array_values(array_unique(array_merge([$wear], $assigned)));
|
||||
}
|
||||
$badge_count = 0;
|
||||
foreach ($assigned as $slug) {
|
||||
if ($slug === '' || !isset($badge_map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$signals[] = [
|
||||
'type' => 'badge',
|
||||
'slug' => $slug,
|
||||
'label' => (string) $badge_map[$slug]['label'],
|
||||
'icon' => 'award',
|
||||
'tone' => (string) ($badge_map[$slug]['tone'] ?? 'neutral'),
|
||||
'icon_url' => (string) ($badge_map[$slug]['icon_url'] ?? ''),
|
||||
'wear' => ($wear !== '' && $slug === $wear),
|
||||
];
|
||||
$badge_count++;
|
||||
if ($badge_count >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $signals;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公开治理提示。
|
||||
*
|
||||
* @return array{state:string,text:string}|array{}
|
||||
*/
|
||||
public function get_profile_notice(int $user_id): array {
|
||||
if ($user_id <= 0 || !class_exists('AKSRT_Settings')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$settings = AKSRT_Settings::get_user_identity_settings();
|
||||
if (empty($settings['report']['enabled']) || empty($settings['report']['public_notice'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$state = sanitize_key((string) get_user_meta($user_id, 'aksrt_user_ban_state', true));
|
||||
if (!in_array($state, ['muted', 'restricted', 'banned'], true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'state' => $state,
|
||||
'text' => (string) $settings['report']['ban_notice_text'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开主页是否显示举报入口。
|
||||
*/
|
||||
public function show_report_entry(): bool {
|
||||
if (!class_exists('AKSRT_Settings')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$settings = AKSRT_Settings::get_user_identity_settings();
|
||||
return !empty($settings['report']['enabled']) && !empty($settings['report']['public_entry']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{accept_email_verified:bool} $verify_settings
|
||||
*/
|
||||
private function is_user_verified(int $user_id, array $verify_settings): bool {
|
||||
$manual = sanitize_key((string) get_user_meta($user_id, 'aksrt_user_verify_status', true));
|
||||
if ($manual === 'verified') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!empty($verify_settings['accept_email_verified']) && class_exists('AKSRT_Auth_Service')) {
|
||||
return AKSRT_Auth_Service::user_is_email_verified($user_id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{source?:string,thresholds?:array<int,int>} $level_settings
|
||||
*/
|
||||
private function get_user_level(int $user_id, array $level_settings): int {
|
||||
$source = (string) ($level_settings['source'] ?? 'integral');
|
||||
if ($source === 'manual_meta') {
|
||||
return max(0, (int) get_user_meta($user_id, 'aksrt_user_level', true));
|
||||
}
|
||||
if (class_exists('AKSRT_User_Level')) {
|
||||
return AKSRT_User_Level::get_level($user_id);
|
||||
}
|
||||
// Fallback: credits_balance thresholds (legacy) if integral class missing.
|
||||
$score = function_exists('aksrt_user_credits') ? (int) aksrt_user_credits($user_id) : 0;
|
||||
$level = 0;
|
||||
foreach ((array) ($level_settings['thresholds'] ?? []) as $candidate => $threshold) {
|
||||
if ($score >= (int) $threshold) {
|
||||
$level = (int) $candidate;
|
||||
}
|
||||
}
|
||||
return $level;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前页面是否为公开用户主页
|
||||
*/
|
||||
public static function is_page(): bool {
|
||||
return function_exists('aksrt_is_user_home_page') && aksrt_is_user_home_page();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
/**
|
||||
* User level + integral XP — Zibll user_level / user_integral parity.
|
||||
*
|
||||
* Meta:
|
||||
* aksrt_level_integral int total XP
|
||||
* aksrt_user_level int resolved level
|
||||
* aksrt_level_integral_detail array reason => [value, integral]
|
||||
* aksrt_level_integral_date_detail array Y-m-d => int
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_User_Level {
|
||||
|
||||
public const META_INTEGRAL = 'aksrt_level_integral';
|
||||
public const META_LEVEL = 'aksrt_user_level';
|
||||
public const META_DETAIL = 'aksrt_level_integral_detail';
|
||||
public const META_DATE_DETAIL = 'aksrt_level_integral_date_detail';
|
||||
public const META_TODAY_FOLLOW = 'aksrt_level_integral_followed';
|
||||
|
||||
/** @var self|null */
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('user_register', [$this, 'on_register'], 20, 1);
|
||||
add_action('wp_login', [$this, 'on_login'], 20, 2);
|
||||
add_action('aksrt_user_signed_in', [$this, 'on_sign_in'], 20, 1);
|
||||
add_action('publish_post', [$this, 'on_publish_post'], 20, 2);
|
||||
add_action('comment_post', [$this, 'on_comment_post'], 20, 3);
|
||||
add_action('aksrt_user_followed', [$this, 'on_followed'], 20, 2);
|
||||
add_action('aksrt_post_liked', [$this, 'on_post_like'], 20, 2);
|
||||
add_action('aksrt_comment_liked', [$this, 'on_comment_like'], 20, 2);
|
||||
add_action('aksrt_post_favorited', [$this, 'on_post_favorite'], 20, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{enabled:bool,public_display:bool,prefix:string,max:int,source:string,level_opt:array<string,mixed>,integral_opt:array<string,mixed>,notify:bool}
|
||||
*/
|
||||
public static function settings(): array {
|
||||
$all = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_all() : [];
|
||||
$enabled = !empty($all['user_level_s']) || !empty($all['user_level_enabled']);
|
||||
$max = max(3, min(30, (int) ($all['user_level_max'] ?? 10)));
|
||||
$source = (string) ($all['user_level_source'] ?? 'integral');
|
||||
if (!in_array($source, ['integral', 'manual_meta'], true)) {
|
||||
$source = 'integral';
|
||||
}
|
||||
$level_opt = $all['user_level_opt'] ?? [];
|
||||
if (is_string($level_opt)) {
|
||||
$decoded = json_decode($level_opt, true);
|
||||
$level_opt = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
if (!is_array($level_opt)) {
|
||||
$level_opt = [];
|
||||
}
|
||||
$integral_opt = $all['user_integral_opt'] ?? [];
|
||||
if (is_string($integral_opt)) {
|
||||
$decoded = json_decode($integral_opt, true);
|
||||
$integral_opt = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
if (!is_array($integral_opt)) {
|
||||
$integral_opt = [];
|
||||
}
|
||||
$prefix = trim((string) ($all['user_level_prefix'] ?? 'Lv.'));
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'public_display' => !isset($all['user_level_public_display']) || !empty($all['user_level_public_display']),
|
||||
'prefix' => $prefix !== '' ? $prefix : 'Lv.',
|
||||
'max' => $max,
|
||||
'source' => $source,
|
||||
'level_opt' => $level_opt,
|
||||
'integral_opt' => $integral_opt,
|
||||
'notify' => !isset($all['user_level_notify']) || !empty($all['user_level_notify']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Catalog of integral earn keys (defaults).
|
||||
*
|
||||
* @return array<string,int>
|
||||
*/
|
||||
public static function integral_defaults(): array {
|
||||
return [
|
||||
'day_max' => 100,
|
||||
'sign_up' => 20,
|
||||
'sign_in' => 5,
|
||||
'followed' => 5,
|
||||
'post_new' => 5,
|
||||
'post_like' => 1,
|
||||
'post_favorite' => 2,
|
||||
'comment_new' => 2,
|
||||
'comment_like' => 1,
|
||||
'bbs_posts_new' => 3,
|
||||
'bbs_score_extra' => 1,
|
||||
'bbs_essence' => 2,
|
||||
'bbs_posts_hot' => 2,
|
||||
'bbs_plate_new' => 2,
|
||||
'bbs_plate_hot' => 2,
|
||||
'bbs_adopt' => 2,
|
||||
'bbs_comment_hot' => 2,
|
||||
];
|
||||
}
|
||||
|
||||
public static function task_points(string $key): int {
|
||||
$cfg = self::settings();
|
||||
$opt = $cfg['integral_opt'];
|
||||
$def = self::integral_defaults();
|
||||
if (array_key_exists($key, $opt)) {
|
||||
return max(0, (int) $opt[$key]);
|
||||
}
|
||||
return max(0, (int) ($def[$key] ?? 0));
|
||||
}
|
||||
|
||||
public static function day_max(): int {
|
||||
$cfg = self::settings();
|
||||
$opt = $cfg['integral_opt'];
|
||||
if (array_key_exists('day_max', $opt)) {
|
||||
return max(0, (int) $opt['day_max']);
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
|
||||
public static function get_integral(int $user_id): int {
|
||||
if ($user_id <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return max(0, (int) get_user_meta($user_id, self::META_INTEGRAL, true));
|
||||
}
|
||||
|
||||
public static function get_level(int $user_id): int {
|
||||
if ($user_id <= 0) {
|
||||
return 0;
|
||||
}
|
||||
$cfg = self::settings();
|
||||
if (!$cfg['enabled']) {
|
||||
return 0;
|
||||
}
|
||||
if ($cfg['source'] === 'manual_meta') {
|
||||
return max(0, (int) get_user_meta($user_id, self::META_LEVEL, true));
|
||||
}
|
||||
return self::resolve_level(self::get_integral($user_id), $cfg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{max:int,level_opt:array} $cfg
|
||||
*/
|
||||
public static function resolve_level(int $integral, ?array $cfg = null): int {
|
||||
$cfg = $cfg ?? self::settings();
|
||||
$max = (int) $cfg['max'];
|
||||
$opt = $cfg['level_opt'];
|
||||
$level = 1;
|
||||
for ($i = 1; $i <= $max; $i++) {
|
||||
$need = (int) ($opt['upgrade_integral_' . $i] ?? ($i === 1 ? 0 : PHP_INT_MAX));
|
||||
if ($integral >= $need) {
|
||||
$level = $i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return max(0, min($max, $level));
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant integral for a task key. Respects day_max.
|
||||
*
|
||||
* @return int|false points granted or false
|
||||
*/
|
||||
public static function add(int $user_id, string $key, int $points = 0) {
|
||||
if ($user_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
$cfg = self::settings();
|
||||
if (!$cfg['enabled'] || $cfg['source'] !== 'integral') {
|
||||
return false;
|
||||
}
|
||||
$key = sanitize_key($key);
|
||||
if ($key === '' || $key === 'day_max') {
|
||||
return false;
|
||||
}
|
||||
if ($points <= 0) {
|
||||
$points = self::task_points($key);
|
||||
}
|
||||
if ($points <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$day_max = self::day_max();
|
||||
$today = gmdate('Y-m-d', current_time('timestamp'));
|
||||
$date_detail = get_user_meta($user_id, self::META_DATE_DETAIL, true);
|
||||
$date_detail = is_array($date_detail) ? $date_detail : [];
|
||||
$today_sum = max(0, (int) ($date_detail[$today] ?? 0));
|
||||
if ($day_max > 0 && $today_sum >= $day_max) {
|
||||
return false;
|
||||
}
|
||||
if ($day_max > 0) {
|
||||
$points = min($points, $day_max - $today_sum);
|
||||
}
|
||||
if ($points <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$before = self::get_integral($user_id);
|
||||
$after = $before + $points;
|
||||
update_user_meta($user_id, self::META_INTEGRAL, $after);
|
||||
|
||||
$date_detail[$today] = $today_sum + $points;
|
||||
// Keep last 90 days.
|
||||
if (count($date_detail) > 90) {
|
||||
krsort($date_detail);
|
||||
$date_detail = array_slice($date_detail, 0, 90, true);
|
||||
}
|
||||
update_user_meta($user_id, self::META_DATE_DETAIL, $date_detail);
|
||||
|
||||
$detail = get_user_meta($user_id, self::META_DETAIL, true);
|
||||
$detail = is_array($detail) ? $detail : [];
|
||||
$prev = is_array($detail[$key] ?? null) ? $detail[$key] : ['value' => 0, 'integral' => 0];
|
||||
$detail[$key] = [
|
||||
'value' => (int) ($prev['value'] ?? 0) + $points,
|
||||
'integral' => (int) ($prev['integral'] ?? 0) + $points,
|
||||
];
|
||||
update_user_meta($user_id, self::META_DETAIL, $detail);
|
||||
|
||||
$old_level = (int) get_user_meta($user_id, self::META_LEVEL, true);
|
||||
$new_level = self::resolve_level($after, $cfg);
|
||||
if ($new_level !== $old_level) {
|
||||
update_user_meta($user_id, self::META_LEVEL, $new_level);
|
||||
/**
|
||||
* @param int $user_id
|
||||
* @param int $new_level
|
||||
* @param int $old_level
|
||||
*/
|
||||
do_action('aksrt_user_level_changed', $user_id, $new_level, $old_level);
|
||||
} else {
|
||||
update_user_meta($user_id, self::META_LEVEL, $new_level);
|
||||
}
|
||||
|
||||
return $points;
|
||||
}
|
||||
|
||||
public function on_register(int $user_id): void {
|
||||
self::add($user_id, 'sign_up');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $user_login
|
||||
* @param WP_User $user
|
||||
*/
|
||||
public function on_login($user_login, $user): void {
|
||||
$user_id = $user instanceof WP_User ? (int) $user->ID : 0;
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
// Once per calendar day for login XP.
|
||||
$today = gmdate('Y-m-d', current_time('timestamp'));
|
||||
$flag = (string) get_user_meta($user_id, '_aksrt_integral_sign_in_day', true);
|
||||
if ($flag === $today) {
|
||||
return;
|
||||
}
|
||||
$granted = self::add($user_id, 'sign_in');
|
||||
if ($granted) {
|
||||
update_user_meta($user_id, '_aksrt_integral_sign_in_day', $today);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_sign_in(int $user_id): void {
|
||||
// Dedicated check-in hook can also grant if separate from login; reuse sign_in key once/day.
|
||||
$this->on_login('', get_userdata($user_id) ?: new WP_User($user_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $post_id
|
||||
* @param WP_Post $post
|
||||
*/
|
||||
public function on_publish_post($post_id, $post): void {
|
||||
if (!$post instanceof WP_Post || $post->post_type !== 'post' || $post->post_status !== 'publish') {
|
||||
return;
|
||||
}
|
||||
// Only when transitioning to publish first time.
|
||||
if ((string) get_post_meta($post_id, '_aksrt_integral_post_new', true) === '1') {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author <= 0) {
|
||||
return;
|
||||
}
|
||||
if (self::add($author, 'post_new')) {
|
||||
update_post_meta($post_id, '_aksrt_integral_post_new', '1');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $comment_id
|
||||
* @param int|string $approved
|
||||
* @param array $commentdata
|
||||
*/
|
||||
public function on_comment_post($comment_id, $approved, $commentdata): void {
|
||||
if ((string) $approved !== '1' && $approved !== 1) {
|
||||
return;
|
||||
}
|
||||
$user_id = (int) ($commentdata['user_id'] ?? 0);
|
||||
if ($user_id <= 0) {
|
||||
$user_id = (int) get_comment($comment_id)->user_id;
|
||||
}
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
self::add($user_id, 'comment_new');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $follower_id
|
||||
* @param int $followed_id
|
||||
*/
|
||||
public function on_followed($follower_id, $followed_id): void {
|
||||
$followed_id = (int) $followed_id;
|
||||
if ($followed_id <= 0) {
|
||||
return;
|
||||
}
|
||||
self::add($followed_id, 'followed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $post_id
|
||||
* @param int $user_id liker
|
||||
*/
|
||||
public function on_post_like($post_id, $user_id): void {
|
||||
$post = get_post((int) $post_id);
|
||||
if (!$post instanceof WP_Post) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author <= 0 || $author === (int) $user_id) {
|
||||
return;
|
||||
}
|
||||
self::add($author, 'post_like');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $comment_id
|
||||
* @param int $user_id
|
||||
*/
|
||||
public function on_comment_like($comment_id, $user_id): void {
|
||||
$c = get_comment((int) $comment_id);
|
||||
if (!$c) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $c->user_id;
|
||||
if ($author <= 0 || $author === (int) $user_id) {
|
||||
return;
|
||||
}
|
||||
self::add($author, 'comment_like');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $post_id
|
||||
* @param int $user_id
|
||||
*/
|
||||
public function on_post_favorite($post_id, $user_id): void {
|
||||
$post = get_post((int) $post_id);
|
||||
if (!$post instanceof WP_Post) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author <= 0 || $author === (int) $user_id) {
|
||||
return;
|
||||
}
|
||||
self::add($author, 'post_favorite');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
/**
|
||||
* User metric accumulators for medal get_type + integral XP.
|
||||
*
|
||||
* Maintains user_meta counters used by AKSRT_User_Badges::metric_value()
|
||||
* and grants level XP / re-evaluates medals on the same events as free points.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_User_Metrics {
|
||||
|
||||
public const META_POST_LIKE_SUM = 'aksrt_posts_like_sum';
|
||||
public const META_COMMENT_LIKE_SUM = 'aksrt_comment_like_sum';
|
||||
public const META_POSTS_VIEWS_SUM = 'aksrt_posts_views_sum';
|
||||
public const META_BBS_ADOPT = 'aksrt_bbs_adopt_count';
|
||||
public const META_BBS_COMMENT_HOT = 'aksrt_bbs_comment_hot_count';
|
||||
public const META_BBS_POST_HOT = 'aksrt_bbs_post_hot_count';
|
||||
public const META_BBS_ESSENCE = 'aksrt_bbs_essence_count';
|
||||
public const META_BBS_SCORE = 'aksrt_bbs_score_sum';
|
||||
public const META_PAY_INCOME = 'aksrt_pay_income_sum';
|
||||
public const META_PAY_REBATE = 'aksrt_pay_rebate_sum';
|
||||
public const META_CHECKIN_ALL_DAY = 'aksrt_checkin_all_day';
|
||||
|
||||
/** @var self|null */
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('aksrt_post_liked', [$this, 'on_post_liked'], 20, 3);
|
||||
add_action('aksrt_comment_liked', [$this, 'on_comment_liked'], 20, 3);
|
||||
add_action('aksrt_post_favorited', [$this, 'on_post_favorited'], 20, 3);
|
||||
add_action('aksrt_user_followed', [$this, 'on_user_followed'], 20, 2);
|
||||
add_action('aksrt_post_viewed', [$this, 'on_post_viewed'], 20, 2);
|
||||
|
||||
add_action('aksrt_forum_topic_featured', [$this, 'on_forum_topic_featured'], 20, 2);
|
||||
add_action('aksrt_forum_reply_adopted', [$this, 'on_forum_reply_adopted'], 20, 3);
|
||||
add_action('aksrt_forum_comment_hot', [$this, 'on_forum_comment_hot'], 20, 2);
|
||||
add_action('aksrt_forum_topic_hot', [$this, 'on_forum_topic_hot'], 20, 2);
|
||||
add_action('aksrt_forum_topic_scored', [$this, 'on_forum_topic_scored'], 20, 3);
|
||||
|
||||
add_action('aksrt_user_signed_in', [$this, 'on_user_signed_in'], 20, 1);
|
||||
add_action('aksrt_creator_income_added', [$this, 'on_creator_income_added'], 20, 3);
|
||||
add_action('aksrt_pay_rebate_added', [$this, 'on_pay_rebate_added'], 20, 3);
|
||||
|
||||
// Free-points / social hooks also feed level XP.
|
||||
add_action('aksrt_post_liked', [$this, 'level_on_post_liked'], 25, 3);
|
||||
add_action('aksrt_comment_liked', [$this, 'level_on_comment_liked'], 25, 3);
|
||||
add_action('aksrt_post_favorited', [$this, 'level_on_post_favorited'], 25, 3);
|
||||
add_action('aksrt_user_followed', [$this, 'level_on_user_followed'], 25, 2);
|
||||
add_action('aksrt_forum_topic_featured', [$this, 'level_on_forum_topic_featured'], 25, 2);
|
||||
add_action('aksrt_forum_reply_adopted', [$this, 'level_on_forum_reply_adopted'], 25, 3);
|
||||
add_action('aksrt_forum_comment_hot', [$this, 'level_on_forum_comment_hot'], 25, 2);
|
||||
add_action('aksrt_forum_topic_hot', [$this, 'level_on_forum_topic_hot'], 25, 2);
|
||||
add_action('aksrt_forum_topic_scored', [$this, 'level_on_forum_topic_scored'], 25, 3);
|
||||
add_action('aksrt_forum_topic_created', [$this, 'level_on_forum_topic_created'], 25, 2);
|
||||
add_action('aksrt_user_signed_in', [$this, 'level_on_user_signed_in'], 25, 1);
|
||||
}
|
||||
|
||||
public static function bump_user_meta(int $user_id, string $meta_key, float $delta = 1.0): float {
|
||||
if ($user_id <= 0 || $meta_key === '' || $delta == 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
$current = (float) get_user_meta($user_id, $meta_key, true);
|
||||
$next = max(0.0, $current + $delta);
|
||||
update_user_meta($user_id, $meta_key, $next);
|
||||
return $next;
|
||||
}
|
||||
|
||||
public static function reevaluate_medals(int $user_id): void {
|
||||
if ($user_id > 0 && class_exists('AKSRT_User_Badges') && method_exists('AKSRT_User_Badges', 'auto_evaluate')) {
|
||||
AKSRT_User_Badges::auto_evaluate($user_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_post_liked(int $post_id, int $actor_id, bool $active): void {
|
||||
$post = get_post($post_id);
|
||||
if (!$post instanceof WP_Post || $post->post_type !== 'post') {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author <= 0 || $author === $actor_id) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($author, self::META_POST_LIKE_SUM, $active ? 1.0 : -1.0);
|
||||
if ($active) {
|
||||
self::reevaluate_medals($author);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_comment_liked(int $comment_id, int $actor_id, bool $active): void {
|
||||
$comment = get_comment($comment_id);
|
||||
if (!$comment) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $comment->user_id;
|
||||
if ($author <= 0 || $author === $actor_id) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($author, self::META_COMMENT_LIKE_SUM, $active ? 1.0 : -1.0);
|
||||
if ($active) {
|
||||
self::reevaluate_medals($author);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_post_favorited(int $post_id, int $actor_id, bool $active): void {
|
||||
unset($post_id, $actor_id, $active);
|
||||
// Favorite is free-points / level only; no dedicated medal get_type.
|
||||
}
|
||||
|
||||
public function on_user_followed(int $follower_id, int $following_id): void {
|
||||
unset($follower_id);
|
||||
if ($following_id > 0) {
|
||||
self::reevaluate_medals($following_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_post_viewed(int $post_id, int $viewer_id = 0): void {
|
||||
unset($viewer_id);
|
||||
$post = get_post($post_id);
|
||||
if (!$post instanceof WP_Post || $post->post_type !== 'post') {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($author, self::META_POSTS_VIEWS_SUM, 1.0);
|
||||
// Cheap throttle: reevaluate every 50 views on author sum.
|
||||
$sum = (float) get_user_meta($author, self::META_POSTS_VIEWS_SUM, true);
|
||||
if (((int) $sum % 50) === 0) {
|
||||
self::reevaluate_medals($author);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $topic_id
|
||||
* @param int $author_id
|
||||
*/
|
||||
public function on_forum_topic_featured($topic_id, $author_id = 0): void {
|
||||
$author_id = (int) $author_id;
|
||||
if ($author_id <= 0) {
|
||||
$post = get_post((int) $topic_id);
|
||||
$author_id = $post ? (int) $post->post_author : 0;
|
||||
}
|
||||
if ($author_id <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($author_id, self::META_BBS_ESSENCE, 1.0);
|
||||
self::reevaluate_medals($author_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $topic_id
|
||||
* @param int $comment_id
|
||||
* @param int $adoptee_id
|
||||
*/
|
||||
public function on_forum_reply_adopted($topic_id, $comment_id, $adoptee_id): void {
|
||||
unset($topic_id, $comment_id);
|
||||
$adoptee_id = (int) $adoptee_id;
|
||||
if ($adoptee_id <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($adoptee_id, self::META_BBS_ADOPT, 1.0);
|
||||
self::reevaluate_medals($adoptee_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $comment_id
|
||||
* @param int $author_id
|
||||
*/
|
||||
public function on_forum_comment_hot($comment_id, $author_id = 0): void {
|
||||
$author_id = (int) $author_id;
|
||||
if ($author_id <= 0) {
|
||||
$c = get_comment((int) $comment_id);
|
||||
$author_id = $c ? (int) $c->user_id : 0;
|
||||
}
|
||||
if ($author_id <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($author_id, self::META_BBS_COMMENT_HOT, 1.0);
|
||||
self::reevaluate_medals($author_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $topic_id
|
||||
* @param int $author_id
|
||||
*/
|
||||
public function on_forum_topic_hot($topic_id, $author_id = 0): void {
|
||||
$author_id = (int) $author_id;
|
||||
if ($author_id <= 0) {
|
||||
$post = get_post((int) $topic_id);
|
||||
$author_id = $post ? (int) $post->post_author : 0;
|
||||
}
|
||||
if ($author_id <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($author_id, self::META_BBS_POST_HOT, 1.0);
|
||||
self::reevaluate_medals($author_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $topic_id
|
||||
* @param int $author_id
|
||||
* @param float $delta
|
||||
*/
|
||||
public function on_forum_topic_scored($topic_id, $author_id, $delta = 0): void {
|
||||
unset($topic_id);
|
||||
$author_id = (int) $author_id;
|
||||
$delta = (float) $delta;
|
||||
if ($author_id <= 0 || $delta == 0.0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($author_id, self::META_BBS_SCORE, $delta);
|
||||
self::reevaluate_medals($author_id);
|
||||
}
|
||||
|
||||
public function on_user_signed_in(int $user_id): void {
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($user_id, self::META_CHECKIN_ALL_DAY, 1.0);
|
||||
self::reevaluate_medals($user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $user_id
|
||||
* @param float $amount
|
||||
* @param array $context
|
||||
*/
|
||||
public function on_creator_income_added($user_id, $amount, $context = []): void {
|
||||
unset($context);
|
||||
$user_id = (int) $user_id;
|
||||
$amount = (float) $amount;
|
||||
if ($user_id <= 0 || $amount <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($user_id, self::META_PAY_INCOME, $amount);
|
||||
self::reevaluate_medals($user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $user_id
|
||||
* @param float $amount
|
||||
* @param array $context
|
||||
*/
|
||||
public function on_pay_rebate_added($user_id, $amount, $context = []): void {
|
||||
unset($context);
|
||||
$user_id = (int) $user_id;
|
||||
$amount = (float) $amount;
|
||||
if ($user_id <= 0 || $amount <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($user_id, self::META_PAY_REBATE, $amount);
|
||||
self::reevaluate_medals($user_id);
|
||||
}
|
||||
|
||||
/* ---------- level XP bridges ---------- */
|
||||
|
||||
private static function level_add(int $user_id, string $key): void {
|
||||
if ($user_id > 0 && class_exists('AKSRT_User_Level')) {
|
||||
AKSRT_User_Level::add($user_id, $key);
|
||||
}
|
||||
}
|
||||
|
||||
public function level_on_post_liked(int $post_id, int $actor_id, bool $active): void {
|
||||
if (!$active) {
|
||||
return;
|
||||
}
|
||||
$post = get_post($post_id);
|
||||
if (!$post instanceof WP_Post) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author > 0 && $author !== $actor_id) {
|
||||
self::level_add($author, 'post_like');
|
||||
}
|
||||
}
|
||||
|
||||
public function level_on_comment_liked(int $comment_id, int $actor_id, bool $active): void {
|
||||
if (!$active) {
|
||||
return;
|
||||
}
|
||||
$c = get_comment($comment_id);
|
||||
if (!$c) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $c->user_id;
|
||||
if ($author > 0 && $author !== $actor_id) {
|
||||
self::level_add($author, 'comment_like');
|
||||
}
|
||||
}
|
||||
|
||||
public function level_on_post_favorited(int $post_id, int $actor_id, bool $active): void {
|
||||
if (!$active) {
|
||||
return;
|
||||
}
|
||||
$post = get_post($post_id);
|
||||
if (!$post instanceof WP_Post) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author > 0 && $author !== $actor_id) {
|
||||
self::level_add($author, 'post_favorite');
|
||||
}
|
||||
}
|
||||
|
||||
public function level_on_user_followed(int $follower_id, int $following_id): void {
|
||||
unset($follower_id);
|
||||
self::level_add((int) $following_id, 'followed');
|
||||
}
|
||||
|
||||
public function level_on_forum_topic_featured($topic_id, $author_id = 0): void {
|
||||
unset($topic_id);
|
||||
self::level_add((int) $author_id, 'bbs_essence');
|
||||
}
|
||||
|
||||
public function level_on_forum_reply_adopted($topic_id, $comment_id, $adoptee_id): void {
|
||||
unset($topic_id, $comment_id);
|
||||
self::level_add((int) $adoptee_id, 'bbs_adopt');
|
||||
}
|
||||
|
||||
public function level_on_forum_comment_hot($comment_id, $author_id = 0): void {
|
||||
unset($comment_id);
|
||||
self::level_add((int) $author_id, 'bbs_comment_hot');
|
||||
}
|
||||
|
||||
public function level_on_forum_topic_hot($topic_id, $author_id = 0): void {
|
||||
unset($topic_id);
|
||||
self::level_add((int) $author_id, 'bbs_posts_hot');
|
||||
}
|
||||
|
||||
public function level_on_forum_topic_scored($topic_id, $author_id, $delta = 0): void {
|
||||
unset($topic_id);
|
||||
if ((float) $delta > 0) {
|
||||
self::level_add((int) $author_id, 'bbs_score_extra');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $user_id
|
||||
* @param int $topic_id
|
||||
*/
|
||||
public function level_on_forum_topic_created($user_id, $topic_id = 0): void {
|
||||
unset($topic_id);
|
||||
self::level_add((int) $user_id, 'bbs_posts_new');
|
||||
}
|
||||
|
||||
public function level_on_user_signed_in(int $user_id): void {
|
||||
// Sign-in XP: use sign_in key once/day via existing login guard path as well.
|
||||
self::level_add($user_id, 'sign_in');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT User Notifications
|
||||
*
|
||||
* Notification CRUD, AJAX handlers, and event-driven notification creators.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
if (!function_exists('aksrt_get_user_notifications')) :
|
||||
/**
|
||||
* 获取用户提醒列表(即时提醒,不持久化)
|
||||
*
|
||||
* 当前通知源:
|
||||
* - 邮箱待验证
|
||||
* - 评论待审核
|
||||
*
|
||||
* @param int $user_id 用户 ID,默认当前用户
|
||||
* @return array<int, array{type:string,title:string,desc:string,url:string,count:int}>
|
||||
*/
|
||||
function aksrt_get_user_notifications(int $user_id = 0): array {
|
||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$items = [];
|
||||
|
||||
$email_verified = true;
|
||||
if (class_exists('AKSRT_Auth_Service')) {
|
||||
$email_verified = AKSRT_Auth_Service::user_is_email_verified($user_id);
|
||||
}
|
||||
|
||||
if (!$email_verified && (!class_exists('AKSRT_Settings') || AKSRT_Settings::notification_enabled('verify_email'))) {
|
||||
$items[] = [
|
||||
'type' => 'verify_email',
|
||||
'title' => __('邮箱待验证', 'aksrt'),
|
||||
'desc' => __('请尽快完成邮箱验证,部分功能需要验证后才能使用。', 'aksrt'),
|
||||
'url' => function_exists('aksrt_get_verify_email_url') ? aksrt_get_verify_email_url($user_id) : home_url('/'),
|
||||
'count' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
$pending_comments = (int) get_comments([
|
||||
'user_id' => $user_id,
|
||||
'count' => true,
|
||||
'status' => 'hold',
|
||||
]);
|
||||
|
||||
if ($pending_comments > 0 && (!class_exists('AKSRT_Settings') || AKSRT_Settings::notification_enabled('pending_comments'))) {
|
||||
$items[] = [
|
||||
'type' => 'pending_comments',
|
||||
'title' => __('评论待审核', 'aksrt'),
|
||||
'desc' => sprintf(__('你有 %d 条评论正在等待审核。', 'aksrt'), $pending_comments),
|
||||
'url' => aksrt_get_user_center_url('comments'),
|
||||
'count' => $pending_comments,
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_notification_type_label')) :
|
||||
/** 通知类型中文标签 */
|
||||
function aksrt_notification_type_label(string $type): string {
|
||||
$map = [
|
||||
'comment_reply' => __('评论回复', 'aksrt'),
|
||||
'comment_approved' => __('评论审核', 'aksrt'),
|
||||
'download_ready' => __('下载就绪', 'aksrt'),
|
||||
'credits_added' => __('积分增加', 'aksrt'),
|
||||
'credits_deducted' => __('积分扣减', 'aksrt'),
|
||||
'verify_email' => __('邮箱验证', 'aksrt'),
|
||||
'pending_comments' => __('待审核评论', 'aksrt'),
|
||||
'notice' => __('通知', 'aksrt'),
|
||||
];
|
||||
return $map[$type] ?? $type;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_user_notification_count')) :
|
||||
/** 获取用户提醒总数 */
|
||||
function aksrt_get_user_notification_count(int $user_id = 0): int {
|
||||
$items = aksrt_get_user_notifications($user_id);
|
||||
$count = 0;
|
||||
foreach ($items as $item) {
|
||||
$count += max(0, (int) ($item['count'] ?? 0));
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_user_inbox_notifications')) :
|
||||
/** 获取用户通知收件箱 */
|
||||
function aksrt_get_user_inbox_notifications(int $user_id = 0): array {
|
||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
if ($user_id <= 0) return [];
|
||||
|
||||
global $wpdb;
|
||||
$table = class_exists('AKSRT_DB_Migrator') ? AKSRT_DB_Migrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
$items = get_user_meta($user_id, 'aksrt_notifications', true);
|
||||
return is_array($items) ? array_values($items) : [];
|
||||
}
|
||||
|
||||
static $migrated = [];
|
||||
if (empty($migrated[$user_id])) {
|
||||
$legacy = get_user_meta($user_id, 'aksrt_notifications', true);
|
||||
if (is_array($legacy) && !empty($legacy)) {
|
||||
foreach ($legacy as $item) {
|
||||
aksrt_add_user_notification($user_id, [
|
||||
'id' => $item['id'] ?? '',
|
||||
'type' => $item['type'] ?? 'notice',
|
||||
'title' => $item['title'] ?? '',
|
||||
'desc' => $item['desc'] ?? '',
|
||||
'url' => $item['url'] ?? '',
|
||||
'read' => !empty($item['read']),
|
||||
'created_at' => $item['created_at'] ?? current_time('mysql', true),
|
||||
]);
|
||||
}
|
||||
delete_user_meta($user_id, 'aksrt_notifications');
|
||||
}
|
||||
$migrated[$user_id] = true;
|
||||
}
|
||||
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT id, type, title, description, target_url, is_read, created_at, read_at
|
||||
FROM {$table}
|
||||
WHERE user_id = %d
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 50",
|
||||
$user_id
|
||||
), ARRAY_A);
|
||||
|
||||
if (!is_array($rows)) return [];
|
||||
|
||||
return array_map(static function ($row) {
|
||||
return [
|
||||
'id' => (string) ($row['id'] ?? ''),
|
||||
'type' => (string) ($row['type'] ?? 'notice'),
|
||||
'title' => (string) ($row['title'] ?? ''),
|
||||
'desc' => (string) ($row['description'] ?? ''),
|
||||
'url' => (string) ($row['target_url'] ?? ''),
|
||||
'read' => !empty($row['is_read']) ? 1 : 0,
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'read_at' => (string) ($row['read_at'] ?? ''),
|
||||
];
|
||||
}, $rows);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_schedule_notification_cleanup')) :
|
||||
/** 注册通知清理定时任务 */
|
||||
function aksrt_schedule_notification_cleanup(): void {
|
||||
if (!wp_next_scheduled('aksrt_notification_cleanup')) {
|
||||
$next = (new DateTimeImmutable('tomorrow 04:10', wp_timezone()))->getTimestamp();
|
||||
wp_schedule_event($next, 'daily', 'aksrt_notification_cleanup');
|
||||
}
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_cleanup_notifications')) :
|
||||
/**
|
||||
* 清理通知表:
|
||||
* - 已读通知保留 30 天
|
||||
* - 未读通知保留 180 天
|
||||
* - 每个用户最多保留最近 200 条通知
|
||||
*/
|
||||
function aksrt_cleanup_notifications(): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = class_exists('AKSRT_DB_Migrator') ? AKSRT_DB_Migrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_notification_settings() : [
|
||||
'read_retention_days' => 30,
|
||||
'unread_retention_days' => 180,
|
||||
'max_per_user' => 200,
|
||||
];
|
||||
|
||||
$read_retention_days = max(1, (int) ($settings['read_retention_days'] ?? 30));
|
||||
$unread_retention_days = max(7, (int) ($settings['unread_retention_days'] ?? 180));
|
||||
$max_per_user = max(20, (int) ($settings['max_per_user'] ?? 200));
|
||||
|
||||
$read_cutoff = (new DateTimeImmutable('-' . $read_retention_days . ' days', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||
$unread_cutoff = (new DateTimeImmutable('-' . $unread_retention_days . ' days', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$table} WHERE is_read = 1 AND created_at < %s",
|
||||
$read_cutoff
|
||||
));
|
||||
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$table} WHERE is_read = 0 AND created_at < %s",
|
||||
$unread_cutoff
|
||||
));
|
||||
|
||||
$user_ids = $wpdb->get_col("SELECT DISTINCT user_id FROM {$table}");
|
||||
if (empty($user_ids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($user_ids as $user_id) {
|
||||
$user_id = (int) $user_id;
|
||||
if ($user_id <= 0) continue;
|
||||
|
||||
$ids = $wpdb->get_col($wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d ORDER BY created_at DESC, id DESC LIMIT %d, 1000",
|
||||
$user_id,
|
||||
$max_per_user
|
||||
));
|
||||
|
||||
if (empty($ids)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ids = array_map('absint', $ids);
|
||||
$ids = array_filter($ids, static function ($id) { return $id > 0; });
|
||||
if (empty($ids)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$wpdb->query("DELETE FROM {$table} WHERE id IN (" . implode(',', $ids) . ")");
|
||||
}
|
||||
}
|
||||
endif;
|
||||
|
||||
add_action('wp', 'aksrt_schedule_notification_cleanup');
|
||||
add_action('aksrt_notification_cleanup', 'aksrt_cleanup_notifications');
|
||||
|
||||
if (!function_exists('aksrt_write_user_inbox_notifications')) :
|
||||
/** 写入用户通知收件箱 */
|
||||
function aksrt_write_user_inbox_notifications(int $user_id, array $items): void {
|
||||
if ($user_id <= 0) return;
|
||||
global $wpdb;
|
||||
$table = class_exists('AKSRT_DB_Migrator') ? AKSRT_DB_Migrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
update_user_meta($user_id, 'aksrt_notifications', array_values(array_slice($items, 0, 50)));
|
||||
return;
|
||||
}
|
||||
|
||||
$wpdb->delete($table, ['user_id' => $user_id], ['%d']);
|
||||
$items = array_values(array_slice($items, 0, 50));
|
||||
foreach ($items as $item) {
|
||||
$wpdb->insert($table, [
|
||||
'user_id' => $user_id,
|
||||
'type' => sanitize_key((string) ($item['type'] ?? 'notice')),
|
||||
'title' => sanitize_text_field((string) ($item['title'] ?? '')),
|
||||
'description' => sanitize_text_field((string) ($item['desc'] ?? '')),
|
||||
'target_url' => esc_url_raw((string) ($item['url'] ?? '')),
|
||||
'is_read' => !empty($item['read']) ? 1 : 0,
|
||||
'created_at' => (string) ($item['created_at'] ?? current_time('mysql', true)),
|
||||
'read_at' => !empty($item['read']) ? current_time('mysql', true) : null,
|
||||
], ['%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s']);
|
||||
}
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_add_user_notification')) :
|
||||
/** 添加一条用户通知 */
|
||||
function aksrt_add_user_notification(int $user_id, array $payload): void {
|
||||
if ($user_id <= 0) return;
|
||||
$type = sanitize_key((string) ($payload['type'] ?? 'notice'));
|
||||
if (class_exists('AKSRT_Settings') && !AKSRT_Settings::notification_enabled($type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = class_exists('AKSRT_DB_Migrator') ? AKSRT_DB_Migrator::table('notification') : '';
|
||||
$created_at = (string) ($payload['created_at'] ?? current_time('mysql', true));
|
||||
|
||||
if ($table === '') {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
array_unshift($items, [
|
||||
'id' => isset($payload['id']) && $payload['id'] !== '' ? sanitize_text_field((string) $payload['id']) : uniqid('notif_', true),
|
||||
'type' => $type,
|
||||
'title' => sanitize_text_field((string) ($payload['title'] ?? '')),
|
||||
'desc' => sanitize_text_field((string) ($payload['desc'] ?? '')),
|
||||
'url' => esc_url_raw((string) ($payload['url'] ?? '')),
|
||||
'read' => !empty($payload['read']) ? 1 : 0,
|
||||
'created_at' => $created_at,
|
||||
]);
|
||||
aksrt_write_user_inbox_notifications($user_id, $items);
|
||||
return;
|
||||
}
|
||||
|
||||
$wpdb->insert($table, [
|
||||
'user_id' => $user_id,
|
||||
'type' => $type,
|
||||
'title' => sanitize_text_field((string) ($payload['title'] ?? '')),
|
||||
'description' => sanitize_text_field((string) ($payload['desc'] ?? '')),
|
||||
'target_url' => esc_url_raw((string) ($payload['url'] ?? '')),
|
||||
'is_read' => !empty($payload['read']) ? 1 : 0,
|
||||
'created_at' => $created_at,
|
||||
'read_at' => !empty($payload['read']) ? current_time('mysql', true) : null,
|
||||
], ['%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s']);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_mark_user_notification_read')) :
|
||||
/** 标记通知已读 */
|
||||
function aksrt_mark_user_notification_read(int $user_id, string $notification_id = '', bool $mark_all = false): void {
|
||||
if ($user_id <= 0) return;
|
||||
global $wpdb;
|
||||
$table = class_exists('AKSRT_DB_Migrator') ? AKSRT_DB_Migrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
foreach ($items as &$item) {
|
||||
if ($mark_all || (($item['id'] ?? '') === $notification_id)) {
|
||||
$item['read'] = 1;
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
aksrt_write_user_inbox_notifications($user_id, $items);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($mark_all) {
|
||||
$wpdb->update(
|
||||
$table,
|
||||
['is_read' => 1, 'read_at' => current_time('mysql', true)],
|
||||
['user_id' => $user_id, 'is_read' => 0],
|
||||
['%d', '%s'],
|
||||
['%d', '%d']
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$wpdb->update(
|
||||
$table,
|
||||
['is_read' => 1, 'read_at' => current_time('mysql', true)],
|
||||
['id' => absint($notification_id), 'user_id' => $user_id],
|
||||
['%d', '%s'],
|
||||
['%d', '%d']
|
||||
);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_delete_user_notification')) :
|
||||
/** 删除一条用户通知 */
|
||||
function aksrt_delete_user_notification(int $user_id, string $notification_id): void {
|
||||
if ($user_id <= 0 || $notification_id === '') return;
|
||||
global $wpdb;
|
||||
$table = class_exists('AKSRT_DB_Migrator') ? AKSRT_DB_Migrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
$items = array_values(array_filter($items, static function ($item) use ($notification_id) {
|
||||
return (string) ($item['id'] ?? '') !== $notification_id;
|
||||
}));
|
||||
aksrt_write_user_inbox_notifications($user_id, $items);
|
||||
return;
|
||||
}
|
||||
$wpdb->delete($table, ['id' => absint($notification_id), 'user_id' => $user_id], ['%d', '%d']);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_delete_user_notifications')) :
|
||||
/** 批量删除用户通知 */
|
||||
function aksrt_delete_user_notifications(int $user_id, array $notification_ids): void {
|
||||
if ($user_id <= 0 || empty($notification_ids)) return;
|
||||
$ids = array_values(array_filter(array_map('absint', $notification_ids), static function ($id) { return $id > 0; }));
|
||||
if (empty($ids)) return;
|
||||
|
||||
global $wpdb;
|
||||
$table = class_exists('AKSRT_DB_Migrator') ? AKSRT_DB_Migrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
$items = array_values(array_filter($items, static function ($item) use ($ids) {
|
||||
return !in_array(absint($item['id'] ?? 0), $ids, true);
|
||||
}));
|
||||
aksrt_write_user_inbox_notifications($user_id, $items);
|
||||
return;
|
||||
}
|
||||
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$table} WHERE user_id = " . (int) $user_id . " AND id IN (" . implode(',', $ids) . ")"
|
||||
);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_user_unread_notification_count')) :
|
||||
/** 获取用户未读通知数 */
|
||||
function aksrt_get_user_unread_notification_count(int $user_id = 0): int {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
$count = 0;
|
||||
foreach ($items as $item) {
|
||||
if (empty($item['read'])) $count++;
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_user_tab_badge_count')) :
|
||||
/** user 底部 Tab 徽标计数:未读持久化通知 + 即时状态提醒 */
|
||||
function aksrt_get_user_tab_badge_count(int $user_id = 0): int {
|
||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (class_exists('AKSRT_Settings') && !AKSRT_Settings::notification_user_badge_enabled()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = aksrt_get_user_unread_notification_count($user_id);
|
||||
if (class_exists('AKSRT_Settings') && AKSRT_Settings::notification_badge_include_stateful()) {
|
||||
$count += aksrt_get_user_notification_count($user_id);
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_user_all_notifications')) :
|
||||
/** 获取用户完整通知列表(持久化通知 + 即时提醒) */
|
||||
function aksrt_get_user_all_notifications(int $user_id = 0): array {
|
||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
if ($user_id <= 0) return [];
|
||||
|
||||
$inbox = aksrt_get_user_inbox_notifications($user_id);
|
||||
$alerts = aksrt_get_user_notifications($user_id);
|
||||
|
||||
$normalized_alerts = [];
|
||||
foreach ($alerts as $alert) {
|
||||
$normalized_alerts[] = [
|
||||
'id' => 'state_' . sanitize_key((string) ($alert['type'] ?? 'notice')),
|
||||
'type' => sanitize_key((string) ($alert['type'] ?? 'notice')),
|
||||
'title' => sanitize_text_field((string) ($alert['title'] ?? '')),
|
||||
'desc' => sanitize_text_field((string) ($alert['desc'] ?? '')),
|
||||
'url' => esc_url_raw((string) ($alert['url'] ?? '')),
|
||||
'read' => 0,
|
||||
'created_at' => current_time('mysql', true),
|
||||
'count' => max(0, (int) ($alert['count'] ?? 0)),
|
||||
'stateful' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
return array_merge($inbox, $normalized_alerts);
|
||||
}
|
||||
endif;
|
||||
|
||||
add_action('wp_ajax_aksrt_mark_notification_read', function () {
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
|
||||
}
|
||||
|
||||
$nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : '';
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403);
|
||||
}
|
||||
|
||||
$notification_id = sanitize_text_field((string) ($_POST['notification_id'] ?? ''));
|
||||
$mark_all = !empty($_POST['mark_all']);
|
||||
aksrt_mark_user_notification_read(get_current_user_id(), $notification_id, $mark_all);
|
||||
wp_send_json_success([
|
||||
'unread' => aksrt_get_user_unread_notification_count(get_current_user_id()),
|
||||
]);
|
||||
});
|
||||
|
||||
add_action('wp_ajax_aksrt_delete_notification', function () {
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
|
||||
}
|
||||
|
||||
$nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : '';
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403);
|
||||
}
|
||||
|
||||
$notification_id = sanitize_text_field((string) ($_POST['notification_id'] ?? ''));
|
||||
if ($notification_id === '') {
|
||||
wp_send_json_error(['message' => __('参数错误', 'aksrt')], 400);
|
||||
}
|
||||
|
||||
aksrt_delete_user_notification(get_current_user_id(), $notification_id);
|
||||
wp_send_json_success([
|
||||
'unread' => aksrt_get_user_unread_notification_count(get_current_user_id()),
|
||||
]);
|
||||
});
|
||||
|
||||
add_action('wp_ajax_aksrt_delete_notifications', function () {
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
|
||||
}
|
||||
|
||||
$nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : '';
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403);
|
||||
}
|
||||
|
||||
$ids = isset($_POST['notification_ids']) ? (array) $_POST['notification_ids'] : [];
|
||||
aksrt_delete_user_notifications(get_current_user_id(), $ids);
|
||||
wp_send_json_success([
|
||||
'unread' => aksrt_get_user_unread_notification_count(get_current_user_id()),
|
||||
]);
|
||||
});
|
||||
|
||||
add_action('wp_ajax_aksrt_check_new_notifications', function () {
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
|
||||
}
|
||||
|
||||
$nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : '';
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403);
|
||||
}
|
||||
|
||||
$unread = aksrt_get_user_unread_notification_count(get_current_user_id());
|
||||
wp_send_json_success([
|
||||
'count' => $unread,
|
||||
'unread' => $unread,
|
||||
]);
|
||||
});
|
||||
add_action('wp_insert_comment', function ($comment_id, $comment) {
|
||||
if (!$comment instanceof WP_Comment) return;
|
||||
if ((int) $comment->comment_parent <= 0) return;
|
||||
if ((int) $comment->comment_approved !== 1) return;
|
||||
if (get_comment_meta($comment_id, 'aksrt_reply_notified', true)) return;
|
||||
|
||||
$parent = get_comment((int) $comment->comment_parent);
|
||||
if (!$parent instanceof WP_Comment) return;
|
||||
$target_user_id = (int) $parent->user_id;
|
||||
if ($target_user_id <= 0 || $target_user_id === (int) $comment->user_id) return;
|
||||
|
||||
$post_id = (int) $comment->comment_post_ID;
|
||||
$url = get_comment_link($comment);
|
||||
aksrt_add_user_notification($target_user_id, [
|
||||
'type' => 'comment_reply',
|
||||
'title' => __('评论收到回复', 'aksrt'),
|
||||
'desc' => sprintf(__('“%s” 回复了你的评论。', 'aksrt'), get_comment_author($comment)),
|
||||
'url' => $url ?: get_permalink($post_id),
|
||||
]);
|
||||
update_comment_meta($comment_id, 'aksrt_reply_notified', 1);
|
||||
}, 20, 2);
|
||||
|
||||
add_action('transition_comment_status', function ($new_status, $old_status, $comment) {
|
||||
if (!$comment instanceof WP_Comment) return;
|
||||
if ($new_status !== 'approve' || $old_status === 'approve') return;
|
||||
|
||||
$comment_owner_id = (int) $comment->user_id;
|
||||
if ($comment_owner_id > 0 && !get_comment_meta($comment->comment_ID, 'aksrt_approved_notified', true)) {
|
||||
aksrt_add_user_notification($comment_owner_id, [
|
||||
'type' => 'comment_approved',
|
||||
'title' => __('评论已通过审核', 'aksrt'),
|
||||
'desc' => __('你的评论已通过审核,现已公开展示。', 'aksrt'),
|
||||
'url' => get_comment_link($comment),
|
||||
]);
|
||||
update_comment_meta($comment->comment_ID, 'aksrt_approved_notified', 1);
|
||||
}
|
||||
|
||||
if ((int) $comment->comment_parent <= 0) return;
|
||||
if (get_comment_meta($comment->comment_ID, 'aksrt_reply_notified', true)) return;
|
||||
|
||||
$parent = get_comment((int) $comment->comment_parent);
|
||||
if (!$parent instanceof WP_Comment) return;
|
||||
$target_user_id = (int) $parent->user_id;
|
||||
if ($target_user_id <= 0 || $target_user_id === (int) $comment->user_id) return;
|
||||
|
||||
aksrt_add_user_notification($target_user_id, [
|
||||
'type' => 'comment_reply',
|
||||
'title' => __('评论收到回复', 'aksrt'),
|
||||
'desc' => sprintf(__('“%s” 回复了你的评论。', 'aksrt'), get_comment_author($comment)),
|
||||
'url' => get_comment_link($comment),
|
||||
]);
|
||||
update_comment_meta($comment->comment_ID, 'aksrt_reply_notified', 1);
|
||||
}, 20, 3);
|
||||
|
||||
add_action('aksrt_credits_added', function ($user_id, $delta, $reason, $new_balance) {
|
||||
if ($user_id <= 0 || $delta <= 0) return;
|
||||
aksrt_add_user_notification((int) $user_id, [
|
||||
'type' => 'credits_added',
|
||||
'title' => __('积分增加', 'aksrt'),
|
||||
'desc' => sprintf(__('积分变动:+%1$d,当前余额 %2$d。', 'aksrt'), (int) $delta, (int) $new_balance),
|
||||
'url' => aksrt_get_user_center_url('credits'),
|
||||
]);
|
||||
}, 10, 4);
|
||||
|
||||
add_action('aksrt_credits_deducted', function ($user_id, $cost, $reason, $new_balance) {
|
||||
if ($user_id <= 0 || $cost <= 0) return;
|
||||
aksrt_add_user_notification((int) $user_id, [
|
||||
'type' => 'credits_deducted',
|
||||
'title' => __('积分扣减', 'aksrt'),
|
||||
'desc' => sprintf(__('积分变动:-%1$d,当前余额 %2$d。', 'aksrt'), (int) $cost, (int) $new_balance),
|
||||
'url' => aksrt_get_user_center_url('credits'),
|
||||
]);
|
||||
}, 10, 4);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
/**
|
||||
* User Restriction / Ban System
|
||||
*
|
||||
* Manages user restrictions (warn, mute, suspend, ban) with scope,
|
||||
* expiration, and enforcement hooks.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_User_Restriction {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
const TYPE_WARN = 'warn';
|
||||
const TYPE_MUTE = 'mute';
|
||||
const TYPE_SUSPEND = 'suspend';
|
||||
const TYPE_BAN = 'ban';
|
||||
|
||||
const SCOPE_GLOBAL = 'global';
|
||||
const SCOPE_FORUM = 'forum';
|
||||
const SCOPE_SHOP = 'shop';
|
||||
const SCOPE_COMMENT = 'comment';
|
||||
|
||||
const TYPES = [self::TYPE_WARN, self::TYPE_MUTE, self::TYPE_SUSPEND, self::TYPE_BAN];
|
||||
const SCOPES = [self::SCOPE_GLOBAL, self::SCOPE_FORUM, self::SCOPE_SHOP, self::SCOPE_COMMENT];
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
// Enforce bans on login
|
||||
add_filter('authenticate', [$this, 'enforce_login_ban'], 999, 3);
|
||||
// Enforce comment mute
|
||||
add_filter('pre_comment_on_post', [$this, 'enforce_comment_mute']);
|
||||
// Enforce forum mute
|
||||
add_filter('aksrt_before_create_topic', [$this, 'enforce_forum_mute']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the restriction table name.
|
||||
*/
|
||||
private function table(): string {
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . 'aksrt_user_restriction';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a restriction to a user.
|
||||
*/
|
||||
public function restrict(int $user_id, string $type, string $reason, string $scope = self::SCOPE_GLOBAL, string $expires_at = '', int $created_by = 0): array {
|
||||
global $wpdb;
|
||||
|
||||
if (!in_array($type, self::TYPES, true)) {
|
||||
return ['success' => false, 'message' => 'Invalid restriction type.'];
|
||||
}
|
||||
if (!in_array($scope, self::SCOPES, true)) {
|
||||
return ['success' => false, 'message' => 'Invalid scope.'];
|
||||
}
|
||||
if ($created_by <= 0) {
|
||||
$created_by = get_current_user_id();
|
||||
}
|
||||
|
||||
$data = [
|
||||
'user_id' => $user_id,
|
||||
'type' => $type,
|
||||
'reason' => sanitize_textarea_field($reason),
|
||||
'scope' => $scope,
|
||||
'expires_at' => $expires_at ? gmdate('Y-m-d H:i:s', strtotime($expires_at)) : null,
|
||||
'created_by' => $created_by,
|
||||
'created_at' => current_time('mysql', true),
|
||||
];
|
||||
|
||||
$result = $wpdb->insert($this->table(), $data);
|
||||
if ($result === false) {
|
||||
return ['success' => false, 'message' => 'Database error.'];
|
||||
}
|
||||
|
||||
// Update user meta for quick lookup
|
||||
$this->sync_user_restriction_meta($user_id);
|
||||
|
||||
return ['success' => true, 'id' => $wpdb->insert_id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a restriction by ID.
|
||||
*/
|
||||
public function remove_restriction(int $restriction_id): bool {
|
||||
global $wpdb;
|
||||
$restriction = $this->get_restriction($restriction_id);
|
||||
if (!$restriction) return false;
|
||||
|
||||
$result = $wpdb->delete($this->table(), ['id' => $restriction_id]);
|
||||
if ($result) {
|
||||
$this->sync_user_restriction_meta((int) $restriction['user_id']);
|
||||
}
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single restriction by ID.
|
||||
*/
|
||||
public function get_restriction(int $restriction_id): ?array {
|
||||
global $wpdb;
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE id = %d",
|
||||
$restriction_id
|
||||
), ARRAY_A);
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active restrictions for a user.
|
||||
*/
|
||||
public function get_user_restrictions(int $user_id, bool $active_only = true): array {
|
||||
global $wpdb;
|
||||
$now = current_time('mysql', true);
|
||||
|
||||
if ($active_only) {
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE user_id = %d AND (expires_at IS NULL OR expires_at > %s) ORDER BY created_at DESC",
|
||||
$user_id, $now
|
||||
), ARRAY_A);
|
||||
} else {
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE user_id = %d ORDER BY created_at DESC",
|
||||
$user_id
|
||||
), ARRAY_A);
|
||||
}
|
||||
|
||||
return $rows ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has an active restriction of a given type and scope.
|
||||
*/
|
||||
public function is_restricted(int $user_id, string $type = '', string $scope = ''): bool {
|
||||
$restrictions = $this->get_user_restrictions($user_id, true);
|
||||
foreach ($restrictions as $r) {
|
||||
if ($type && $r['type'] !== $type) continue;
|
||||
if ($scope && $r['scope'] !== $scope && $r['scope'] !== self::SCOPE_GLOBAL) continue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is banned (global).
|
||||
*/
|
||||
public function is_banned(int $user_id): bool {
|
||||
return $this->is_restricted($user_id, self::TYPE_BAN, self::SCOPE_GLOBAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active ban restriction for a user (if any).
|
||||
*/
|
||||
public function get_active_ban(int $user_id): ?array {
|
||||
$restrictions = $this->get_user_restrictions($user_id, true);
|
||||
foreach ($restrictions as $r) {
|
||||
if ($r['type'] === self::TYPE_BAN && $r['scope'] === self::SCOPE_GLOBAL) {
|
||||
return $r;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce ban on login.
|
||||
*/
|
||||
public function enforce_login_ban($user, string $username, string $password) {
|
||||
if (is_wp_error($user) || !$user instanceof WP_User) {
|
||||
return $user;
|
||||
}
|
||||
if ($this->is_banned($user->ID)) {
|
||||
$ban = $this->get_active_ban($user->ID);
|
||||
$message = '您的账号已被封禁。';
|
||||
if (!empty($ban['reason'])) {
|
||||
$message .= '原因:' . $ban['reason'];
|
||||
}
|
||||
return new WP_Error('user_banned', $message);
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce comment mute.
|
||||
*/
|
||||
public function enforce_comment_mute($post_id) {
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id > 0 && $this->is_restricted($user_id, self::TYPE_MUTE, self::SCOPE_COMMENT)) {
|
||||
wp_die('您已被限制评论。', '评论限制', ['response' => 403]);
|
||||
}
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce forum mute.
|
||||
*/
|
||||
public function enforce_forum_mute($data) {
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id > 0 && $this->is_restricted($user_id, self::TYPE_MUTE, self::SCOPE_FORUM)) {
|
||||
return new WP_Error('forum_muted', '您已被限制在论坛发言。');
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync restriction meta for quick lookup.
|
||||
*/
|
||||
private function sync_user_restriction_meta(int $user_id): void {
|
||||
$active = $this->get_user_restrictions($user_id, true);
|
||||
$types = array_unique(array_column($active, 'type'));
|
||||
update_user_meta($user_id, '_aksrt_active_restrictions', $types);
|
||||
update_user_meta($user_id, '_aksrt_is_banned', in_array(self::TYPE_BAN, $types, true) ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restriction stats for admin overview.
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
global $wpdb;
|
||||
$now = current_time('mysql', true);
|
||||
$table = $this->table();
|
||||
|
||||
$total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
|
||||
$active = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE expires_at IS NULL OR expires_at > %s", $now
|
||||
));
|
||||
$banned = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE type = %s AND (expires_at IS NULL OR expires_at > %s)",
|
||||
self::TYPE_BAN, $now
|
||||
));
|
||||
|
||||
return ['total' => $total, 'active' => $active, 'banned' => $banned];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user