feat(identity): tip QR upload, metrics engine, comment moderate caps

Close identity 1:1 gaps after p1: user-center weixin/alipay tip QR
upload+save, AKSRT_User_Metrics counters for medal get_type and level
XP bridges, forum/revenue/signin hooks, comment moderate via user_caps,
and clearer free-task deep links/status CTAs.
This commit is contained in:
kate522 committed 2026-07-17 01:52:35 +08:00
1 parent 4e3eb0414d
commit 487681a6c4
18 files changed
+854 -36

No files matched your search

+1 -3
View File
@@ -67,9 +67,7 @@ class AKSRT_Comment_Actions {
register_rest_route('aksrt/v1', '/comment/moderate', [
'methods' => 'POST',
'callback' => [$this, 'moderate_comment'],
'permission_callback' => function () {
return current_user_can('moderate_comments');
},
'permission_callback' => [$this, 'check_moderate_permission'],
'args' => [
'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
'status' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
+9 -1
View File
@@ -64,7 +64,15 @@ class AKSRT_Rebate {
'note' => $note,
]);
return $result ? (int) $wpdb->insert_id : 0;
$id = $result ? (int) $wpdb->insert_id : 0;
if ($id > 0) {
do_action('aksrt_pay_rebate_added', $user_id, $amount, [
'rebate_id' => $id,
'order_number' => $order_number,
'type' => $type,
]);
}
return $id;
}
/**
+9 -1
View File
@@ -39,7 +39,15 @@ class AKSRT_Revenue {
'created_at' => current_time('mysql'),
], ['%d', '%s', '%f', '%s', '%s', '%s']);
return (int) $wpdb->insert_id;
$id = (int) $wpdb->insert_id;
if ($id > 0) {
do_action('aksrt_creator_income_added', $user_id, $amount, [
'order_number' => $order_number,
'type' => $type,
'revenue_id' => $id,
]);
}
return $id;
}
/**
+2
View File
@@ -130,6 +130,8 @@ class AKSRT_Signin {
return new WP_Error('aksrt_signin_dup', __('签到失败', 'aksrt'), ['status' => 409]);
}
do_action('aksrt_user_signed_in', $user_id);
if ($reward > 0) {
if (class_exists('AKSRT_Points_Free')) {
$granted = AKSRT_Points_Free::grant_amount($user_id, $reward, 'sign_in', ['type' => 'user', 'id' => $user_id]);
+20 -5
View File
@@ -477,11 +477,10 @@ class AKSRT_User_Badges {
return (float) $counts;
case 'followed':
$meta = get_user_meta($user_id, 'followed-user', true);
if (!is_array($meta)) {
$meta = get_user_meta($user_id, 'aksrt_followed_users', true);
if (class_exists('AKSRT_Follow')) {
return (float) AKSRT_Follow::get_instance()->get_follower_count($user_id);
}
return is_array($meta) ? (float) count($meta) : 0.0;
return 0.0;
case 'like':
return (float) get_user_meta($user_id, 'aksrt_posts_like_sum', true);
@@ -505,7 +504,23 @@ class AKSRT_User_Badges {
return (float) get_user_meta($user_id, 'aksrt_bbs_score_sum', true);
case 'views':
return (float) get_user_meta($user_id, 'aksrt_posts_views_sum', true);
$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);
+361
View File
@@ -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');
}
}
+53
View File
@@ -942,6 +942,59 @@ add_action('wp_ajax_aksrt_save_profile', function () {
wp_send_json_success(['message' => __('保存成功!', 'aksrt')]);
});
// ============================================================
// AJAX: 上传打赏收款码
// ============================================================
add_action('wp_ajax_aksrt_upload_reward_qr', function () {
if (!isset($_POST['_profile_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_profile_nonce'])), 'aksrt_profile_save')) {
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
}
$user_id = get_current_user_id();
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
if (function_exists('aksrt_rewards_enabled') && !aksrt_rewards_enabled()) {
wp_send_json_error(['message' => __('打赏功能未启用', 'aksrt')]);
}
$kind = isset($_POST['kind']) ? sanitize_key((string) $_POST['kind']) : '';
if (!in_array($kind, ['weixin', 'alipay'], true)) {
wp_send_json_error(['message' => __('无效类型', 'aksrt')]);
}
if (empty($_FILES['file']['name'])) {
wp_send_json_error(['message' => __('请选择图片', 'aksrt')]);
}
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$file = $_FILES['file'];
$check = wp_check_filetype_and_ext($file['tmp_name'], $file['name']);
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$mime = (string) ($check['type'] ?? $file['type'] ?? '');
if ($mime === '' || !in_array($mime, $allowed, true)) {
wp_send_json_error(['message' => __('仅支持 JPG/PNG/GIF/WebP 图片', 'aksrt')]);
}
if (!empty($file['size']) && (int) $file['size'] > 2 * 1024 * 1024) {
wp_send_json_error(['message' => __('图片不能超过 2MB', 'aksrt')]);
}
$attachment_id = media_handle_upload('file', 0);
if (is_wp_error($attachment_id)) {
wp_send_json_error(['message' => $attachment_id->get_error_message()]);
}
$meta = $kind === 'weixin' ? 'aksrt_rewards_weixin' : 'aksrt_rewards_alipay';
update_user_meta($user_id, $meta, (int) $attachment_id);
$url = wp_get_attachment_image_url((int) $attachment_id, 'full');
wp_send_json_success([
'message' => __('上传成功', 'aksrt'),
'id' => (int) $attachment_id,
'url' => is_string($url) ? $url : '',
'kind' => $kind,
]);
});
// ============================================================
// AJAX: 发送邮箱验证码(修改密码/更改邮箱通用)
// ============================================================
@@ -309,6 +309,7 @@ class AKSRT_Forum_Comment_Service {
update_comment_meta($comment_id, '_aksrt_adopted', '1');
update_post_meta($topic_id, '_aksrt_adopted_comment', $comment_id);
do_action('aksrt_forum_reply_adopted', $topic_id, $comment_id, (int) $comment->user_id);
update_post_meta($topic_id, '_aksrt_question_status', 1); // QUESTION_SOLVED
$adoptee_id = (int) $comment->user_id;
@@ -507,6 +508,8 @@ class AKSRT_Forum_Comment_Service {
}
update_comment_meta($comment_id, '_aksrt_is_hot', '1');
$hot_author = (int) (get_comment($comment_id)->user_id ?? 0);
do_action('aksrt_forum_comment_hot', $comment_id, $hot_author);
// Credit the comment author (free-task day cap).
$author_id = (int) $comment->user_id;
@@ -121,6 +121,7 @@ class AKSRT_Forum_Score_Service {
$total = (int) get_post_meta($topic_id, '_aksrt_score', true);
$total += $amount;
update_post_meta($topic_id, '_aksrt_score', $total);
do_action('aksrt_forum_topic_scored', $topic_id, (int) $author_id, (float) $amount);
$reputation = (int) get_user_meta($author_id, 'aksrt_reputation', true);
$reputation += $amount;
@@ -236,6 +237,7 @@ class AKSRT_Forum_Score_Service {
if ($author_id <= 0) {
return;
}
do_action('aksrt_forum_topic_hot', $topic_id, (int) $author_id);
update_post_meta($topic_id, '_aksrt_hot_rewarded', '1');
$config = $this->forum->get_config();
$amount = (int) ($config['credits']['posts_hot'] ?? 5);