diff --git a/assets/css/routes/user-center/settings.css b/assets/css/routes/user-center/settings.css
index 37f5c53b..7dcb58f1 100644
--- a/assets/css/routes/user-center/settings.css
+++ b/assets/css/routes/user-center/settings.css
@@ -325,3 +325,37 @@
color: var(--color-ink-4);
background: transparent;
}
+
+
+/* Tip QR upload on profile */
+.aksrt-reward-upload-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 16px;
+ margin-top: 12px;
+}
+.aksrt-reward-upload__preview {
+ min-height: 140px;
+ border: 1px dashed rgba(127,127,127,.35);
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ margin: 8px 0;
+ background: rgba(127,127,127,.04);
+}
+.aksrt-reward-upload__preview img {
+ max-width: 100%;
+ max-height: 180px;
+ object-fit: contain;
+ display: block;
+}
+.aksrt-reward-upload__file {
+ display: none;
+}
+.aksrt-reward-upload .btn-sm,
+.aksrt-reward-upload .btn-primary {
+ margin-right: 8px;
+ margin-top: 4px;
+}
diff --git a/assets/js/routes/user-center.js b/assets/js/routes/user-center.js
index 310ac0c6..4c1b84c9 100644
--- a/assets/js/routes/user-center.js
+++ b/assets/js/routes/user-center.js
@@ -120,6 +120,82 @@
}
/* ==================== 个人信息保存 ==================== */
+
+ /* ==================== 打赏收款码上传 ==================== */
+ function initRewardQrUpload() {
+ var form = document.getElementById('profile-form');
+ if (!form) return;
+ var nonceInput = form.querySelector('input[name="_profile_nonce"]');
+ var nonce = nonceInput ? nonceInput.value : '';
+
+ function setPreview(kind, url) {
+ var box = form.querySelector('[data-reward-preview="' + kind + '"]');
+ if (!box) return;
+ if (url) {
+ box.innerHTML = '';
+ } else {
+ box.innerHTML = '未上传';
+ }
+ }
+
+ form.querySelectorAll('[data-reward-upload]').forEach(function (btn) {
+ btn.addEventListener('click', function () {
+ var kind = btn.getAttribute('data-reward-upload');
+ var fileInput = form.querySelector('input[data-reward-kind="' + kind + '"]');
+ if (fileInput) fileInput.click();
+ });
+ });
+
+ form.querySelectorAll('[data-reward-clear]').forEach(function (btn) {
+ btn.addEventListener('click', function () {
+ var kind = btn.getAttribute('data-reward-clear');
+ var hidden = form.querySelector('#rewards-' + kind);
+ var fileInput = form.querySelector('input[data-reward-kind="' + kind + '"]');
+ if (hidden) hidden.value = '';
+ if (fileInput) fileInput.value = '';
+ setPreview(kind, '');
+ });
+ });
+
+ form.querySelectorAll('input[data-reward-kind]').forEach(function (input) {
+ input.addEventListener('change', function () {
+ var kind = input.getAttribute('data-reward-kind');
+ var file = input.files && input.files[0];
+ if (!file || !kind) return;
+ if (file.size > 2 * 1024 * 1024) {
+ showToast('图片不能超过 2MB');
+ input.value = '';
+ return;
+ }
+ var body = new FormData();
+ body.append('action', 'aksrt_upload_reward_qr');
+ body.append('_profile_nonce', nonce);
+ body.append('kind', kind);
+ body.append('file', file);
+ showToast(i18n.saving || '上传中…');
+ fetch(ajaxUrl, { method: 'POST', body: body, credentials: 'same-origin' })
+ .then(function (r) { return r.json(); })
+ .then(function (res) {
+ if (res && res.success && res.data) {
+ var hidden = form.querySelector('#rewards-' + kind);
+ if (hidden) hidden.value = String(res.data.id || '');
+ setPreview(kind, res.data.url || '');
+ showToast(res.data.message || '上传成功');
+ } else {
+ showToast((res && res.data && res.data.message) || '上传失败');
+ }
+ })
+ .catch(function () {
+ showToast(i18n.networkError || '网络错误');
+ })
+ .finally(function () {
+ input.value = '';
+ });
+ });
+ });
+ }
+
+
function initProfileForm() {
var form = document.getElementById('profile-form');
if (!form) return;
@@ -676,6 +752,7 @@
document.addEventListener('DOMContentLoaded', function() {
initDashboardNavigation();
initProfileForm();
+ initRewardQrUpload();
initUnfavorite();
initNotifications();
initSettings();
@@ -683,6 +760,7 @@
} else {
initDashboardNavigation();
initProfileForm();
+ initRewardQrUpload();
initUnfavorite();
initNotifications();
initSettings();
diff --git a/functions.php b/functions.php
index ec0f108a..11c2bd62 100644
--- a/functions.php
+++ b/functions.php
@@ -373,6 +373,9 @@ if (class_exists('AKSRT_User_Badges_Admin')) {
if (class_exists('AKSRT_User_Level')) {
AKSRT_User_Level::get_instance()->init();
}
+if (class_exists('AKSRT_User_Metrics')) {
+ AKSRT_User_Metrics::get_instance()->init();
+}
// WordPress 原生侧栏:支付/订单运营中心(zibll zibpay 对齐)
if (is_admin() && class_exists('AKSRT_Admin_Pay_Menus')) {
diff --git a/inc/bootstrap.php b/inc/bootstrap.php
index 31c7d2c6..cb5926db 100644
--- a/inc/bootstrap.php
+++ b/inc/bootstrap.php
@@ -223,6 +223,7 @@ require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-balance-admin.php';
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-badges.php';
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-badges-admin.php';
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-level.php';
+require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-metrics.php';
// 加载数据表迁移器(CMS / 积分 / 下载 token)
require_once AKSRT_DIR . '/inc/classes/db-traits/trait-aksrt-db-migrator-part-2.php';
diff --git a/inc/classes/class-aksrt-comment-actions.php b/inc/classes/class-aksrt-comment-actions.php
index 712188fc..b1df3767 100644
--- a/inc/classes/class-aksrt-comment-actions.php
+++ b/inc/classes/class-aksrt-comment-actions.php
@@ -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'],
diff --git a/inc/classes/class-aksrt-rebate.php b/inc/classes/class-aksrt-rebate.php
index f79ab8a4..4df9520f 100644
--- a/inc/classes/class-aksrt-rebate.php
+++ b/inc/classes/class-aksrt-rebate.php
@@ -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;
}
/**
diff --git a/inc/classes/class-aksrt-revenue.php b/inc/classes/class-aksrt-revenue.php
index a05314e5..a31df8a2 100644
--- a/inc/classes/class-aksrt-revenue.php
+++ b/inc/classes/class-aksrt-revenue.php
@@ -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;
}
/**
diff --git a/inc/classes/class-aksrt-signin.php b/inc/classes/class-aksrt-signin.php
index 4ebd0431..e939acdf 100644
--- a/inc/classes/class-aksrt-signin.php
+++ b/inc/classes/class-aksrt-signin.php
@@ -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]);
diff --git a/inc/classes/class-aksrt-user-badges.php b/inc/classes/class-aksrt-user-badges.php
index f018a9d7..fc2854e4 100644
--- a/inc/classes/class-aksrt-user-badges.php
+++ b/inc/classes/class-aksrt-user-badges.php
@@ -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);
diff --git a/inc/classes/class-aksrt-user-metrics.php b/inc/classes/class-aksrt-user-metrics.php
new file mode 100644
index 00000000..ce95910b
--- /dev/null
+++ b/inc/classes/class-aksrt-user-metrics.php
@@ -0,0 +1,361 @@
+ 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');
+ }
+}
diff --git a/inc/classes/class-aksrt-user.php b/inc/classes/class-aksrt-user.php
index 85d4b666..a7f45f0f 100644
--- a/inc/classes/class-aksrt-user.php
+++ b/inc/classes/class-aksrt-user.php
@@ -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: 发送邮箱验证码(修改密码/更改邮箱通用)
// ============================================================
diff --git a/inc/classes/forum/class-aksrt-forum-comment-service.php b/inc/classes/forum/class-aksrt-forum-comment-service.php
index a934762e..008500c3 100644
--- a/inc/classes/forum/class-aksrt-forum-comment-service.php
+++ b/inc/classes/forum/class-aksrt-forum-comment-service.php
@@ -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;
diff --git a/inc/classes/forum/class-aksrt-forum-score-service.php b/inc/classes/forum/class-aksrt-forum-score-service.php
index 46a6dddf..3d47142a 100644
--- a/inc/classes/forum/class-aksrt-forum-score-service.php
+++ b/inc/classes/forum/class-aksrt-forum-score-service.php
@@ -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);
diff --git a/inc/helpers/aksrt-frontend-tags.php b/inc/helpers/aksrt-frontend-tags.php
index 330a736f..8558d9b4 100644
--- a/inc/helpers/aksrt-frontend-tags.php
+++ b/inc/helpers/aksrt-frontend-tags.php
@@ -604,6 +604,7 @@ if (!function_exists('aksrt_set_post_views')) :
if (function_exists('aksrt_update_post_counter')) {
aksrt_update_post_counter($post_id, '_aksrt_res_view_count', 1);
aksrt_update_post_counter($post_id, 'aksrt_post_views', 1);
+ do_action('aksrt_post_viewed', (int) $post_id, get_current_user_id());
} else {
$resource_count = (int) get_post_meta($post_id, '_aksrt_res_view_count', true);
update_post_meta($post_id, '_aksrt_res_view_count', $resource_count + 1);
@@ -613,6 +614,7 @@ if (!function_exists('aksrt_set_post_views')) :
} else {
if (function_exists('aksrt_update_post_counter')) {
aksrt_update_post_counter($post_id, 'aksrt_post_views', 1);
+ do_action('aksrt_post_viewed', (int) $post_id, get_current_user_id());
} else {
$count = (int) get_post_meta($post_id, 'aksrt_post_views', true);
update_post_meta($post_id, 'aksrt_post_views', $count + 1);
diff --git a/inc/helpers/aksrt-user-center.php b/inc/helpers/aksrt-user-center.php
index 1fa11113..69c96edb 100644
--- a/inc/helpers/aksrt-user-center.php
+++ b/inc/helpers/aksrt-user-center.php
@@ -91,6 +91,32 @@ if (!function_exists('aksrt_save_user_center_profile')) {
}
update_user_meta($user_id, 'aksrt_user_contacts', aksrt_normalize_user_contacts($input['contacts'] ?? []));
+
+ // Tip QR images (attachment id or URL). Empty clears.
+ if (array_key_exists('rewards_weixin', $input) || array_key_exists('rewards_alipay', $input)) {
+ foreach (['rewards_weixin' => 'aksrt_rewards_weixin', 'rewards_alipay' => 'aksrt_rewards_alipay'] as $field => $meta) {
+ if (!array_key_exists($field, $input)) {
+ continue;
+ }
+ $raw = is_string($input[$field]) ? trim(wp_unslash($input[$field])) : '';
+ if ($raw === '' || $raw === '0') {
+ delete_user_meta($user_id, $meta);
+ continue;
+ }
+ if (ctype_digit($raw)) {
+ $att = (int) $raw;
+ if ($att > 0 && wp_attachment_is_image($att)) {
+ update_user_meta($user_id, $meta, $att);
+ }
+ continue;
+ }
+ $url = esc_url_raw($raw);
+ if ($url !== '') {
+ update_user_meta($user_id, $meta, $url);
+ }
+ }
+ }
+
return true;
}
}
diff --git a/inc/helpers/user-center-renderers/credits-points-tasks.php b/inc/helpers/user-center-renderers/credits-points-tasks.php
index 86e1b1e2..fd4eaacf 100644
--- a/inc/helpers/user-center-renderers/credits-points-tasks.php
+++ b/inc/helpers/user-center-renderers/credits-points-tasks.php
@@ -52,22 +52,48 @@ $aksrt_points_task_action = static function (string $key): array {
'href' => $submit !== '' ? $submit : $home,
];
case 'comment_new':
- return [
- 'type' => 'link',
- 'label' => __('去完成', 'aksrt'),
- 'href' => $home,
- ];
- case 'login':
- case 'post_like':
- case 'post_favorite':
- case 'followed':
- case 'comment_like':
- return [
- 'type' => 'link',
- 'label' => __('去看看', 'aksrt'),
- 'href' => $home,
- ];
- case 'bbs_posts_new':
+ $posts = get_posts(['numberposts' => 1, 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC']);
+ $href = $home;
+ if (!empty($posts[0]) && $posts[0] instanceof WP_Post) {
+ $link = get_permalink($posts[0]);
+ if (is_string($link) && $link !== '') {
+ $href = $link . '#comments';
+ }
+ }
+ return [
+ 'type' => 'link',
+ 'label' => __('去完成', 'aksrt'),
+ 'href' => $href,
+ ];
+ case 'login':
+ return [
+ 'type' => 'none',
+ 'label' => __('登录即可领取', 'aksrt'),
+ ];
+ case 'post_like':
+ case 'post_favorite':
+ case 'comment_like':
+ $posts = get_posts(['numberposts' => 1, 'post_status' => 'publish', 'orderby' => 'comment_count', 'order' => 'DESC']);
+ $href = $home;
+ if (!empty($posts[0]) && $posts[0] instanceof WP_Post) {
+ $link = get_permalink($posts[0]);
+ if (is_string($link) && $link !== '') {
+ $href = $link;
+ }
+ }
+ return [
+ 'type' => 'link',
+ 'label' => __('去看看', 'aksrt'),
+ 'href' => $href,
+ ];
+ case 'followed':
+ $center = function_exists('aksrt_get_user_center_url') ? (string) aksrt_get_user_center_url() : $home;
+ return [
+ 'type' => 'link',
+ 'label' => __('去主页', 'aksrt'),
+ 'href' => $center !== '' ? $center : $home,
+ ];
+ case 'bbs_posts_new':
case 'bbs_reply':
if (!$forum_on) {
return ['type' => 'none', 'label' => ''];
@@ -180,16 +206,19 @@ $pane = isset($points_tasks_pane) ? sanitize_key((string) $points_tasks_pane)