refactor(autoload): migrate theme classes to Composer PSR-4

Move 288 types from global AKSRT_* + inc/classes manual requires into
namespace AKSRT\{Domain} under src/AKSRT/, wire composer.json psr-4,
and slim bootstrap to helpers/modules/hooks only. Split multi-class
widget/gateway files, update admin contract audit paths, and document
the layout in ADR/README.
This commit is contained in:
kate522 committed 2026-07-17 13:29:22 +08:00
1 parent 8f349b40fb
commit aaf458af1c
404 files changed
+13602 -7705

No files matched your search

+550
View File
@@ -0,0 +1,550 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\User;
defined('ABSPATH') || exit;
class 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\Settings')) {
return false;
}
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
return !empty($settings['badge']['enabled']);
}
public static function public_display(): bool {
if (!class_exists('\AKSRT\Settings\Settings')) {
return false;
}
$settings = \AKSRT\Settings\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\Settings')) {
return [];
}
$settings = \AKSRT\Settings\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\Settings')) {
return;
}
$settings = \AKSRT\Settings\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('Follow')) {
return (float) 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);
}
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\User;
defined('ABSPATH') || exit;
class BadgesAdmin {
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('Badges') && Badges::can_manually_set(get_current_user_id());
}
public function render_section(\WP_User $user): void {
if (!class_exists('Badges') || !Badges::enabled()) {
return;
}
$dict = Badges::get_dictionary();
if (!$dict) {
return;
}
$owned = Badges::get_owned((int) $user->ID);
$wear = 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('Badges') || !Badges::enabled()) {
return;
}
$raw = isset($_POST['aksrt_user_badges']) ? (array) wp_unslash($_POST['aksrt_user_badges']) : [];
$slugs = array_map('sanitize_key', $raw);
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, Badges::META_WEAR);
} else {
$res = Badges::wear($user_id, $wear);
if (is_wp_error($res)) {
// leave owned as-is; wear ignored if invalid
delete_user_meta($user_id, Badges::META_WEAR);
}
}
}
}
+162
View File
@@ -0,0 +1,162 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\User;
defined('ABSPATH') || exit;
class BalanceAdmin {
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\Credits\Balance') || !\AKSRT\Credits\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\Credits\Balance')) {
return '—';
}
return '<strong>' . esc_html(\AKSRT\Credits\Balance::format(\AKSRT\Credits\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\Credits\Balance') || !\AKSRT\Credits\Balance::enabled()) {
return;
}
$balance = \AKSRT\Credits\Balance::balance((int) $user->ID);
$can = $this->can_manage();
$log = \AKSRT\Credits\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\Credits\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\Credits\Balance::format((float) $row['delta'])); ?></td>
<td><?php echo esc_html(\AKSRT\Credits\Balance::format((float) ($row['balance_after'] ?? 0))); ?></td>
<td><?php echo esc_html(\AKSRT\Credits\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\Credits\Balance') || !\AKSRT\Credits\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\Credits\Balance::add($user_id, $delta, 'admin', ['type' => 'user', 'id' => get_current_user_id()], $reason);
} else {
$result = \AKSRT\Credits\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);
}
}
+158
View File
@@ -0,0 +1,158 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\User;
defined('ABSPATH') || exit;
class 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\Settings')
? \AKSRT\Settings\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\Settings')
? sprintf(__('我的%s', 'aksrt'), \AKSRT\Settings\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\Settings') && \AKSRT\Settings\Settings::credits_enabled();
$balance_on = class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\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\Settings') && \AKSRT\Settings\Settings::credits_enabled();
$balance_on = class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\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();
}
}
+312
View File
@@ -0,0 +1,312 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\User;
defined('ABSPATH') || exit;
class CreditsAdmin {
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\Settings') ? \AKSRT\Settings\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\Credits')) {
return '—';
}
$balance = \AKSRT\Credits\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\Payment\Revenue')) {
$stats = \AKSRT\Payment\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\Payment\Rebate')) {
$list = \AKSRT\Payment\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\Settings') ? \AKSRT\Settings\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\Credits')) {
return;
}
$balance = \AKSRT\Credits\Credits::balance((int) $user->ID);
$can_manage = $this->can_manage_credits();
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\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\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\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\Settings') ? \AKSRT\Settings\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\Credits::add($user_id, $delta, 'admin', ['type' => 'user', 'id' => get_current_user_id()], $reason);
} else {
$result = \AKSRT\Credits\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);
}
}
+180
View File
@@ -0,0 +1,180 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\User;
defined('ABSPATH') || exit;
class 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\Core\DBMigrator::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)),
];
}
}
+307
View File
@@ -0,0 +1,307 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\User;
defined('ABSPATH') || exit;
class 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\Settings')
? sprintf(__('我的%s', 'aksrt'), \AKSRT\Settings\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\Settings')
? \AKSRT\Settings\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\Settings') || !\AKSRT\Settings\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\Settings')) {
return [];
}
$settings = \AKSRT\Settings\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('Badges')) {
$list = 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\Settings')) {
return [];
}
$settings = \AKSRT\Settings\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\Settings')) {
return false;
}
$settings = \AKSRT\Settings\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('Level')) {
return 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();
}
}
+373
View File
@@ -0,0 +1,373 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\User;
defined('ABSPATH') || exit;
class 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\Settings') ? \AKSRT\Settings\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');
}
}
+358
View File
@@ -0,0 +1,358 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\User;
defined('ABSPATH') || exit;
class 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('Badges') && method_exists('Badges', 'auto_evaluate')) {
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('Level')) {
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');
}
}
+239
View File
@@ -0,0 +1,239 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\User;
defined('ABSPATH') || exit;
class 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];
}
}