Files
wordpress-theme-aksrt/src/Settings/MailNotifications.php
T
kate522andClaude Fable 5 5febdb35c8 feat(ticket): record urges in thread, show urge count in admin, mail admins
- aksrt_ticket gains urge_count / last_urge_time (schema 3.1.0)
- cooldown moves from transient to an atomic conditional UPDATE
- urge inserts a system reply rendered as a centered notice (not quotable)
- admin list badge + info card row for urge count / last time
- new ticket_urge_admin mail event via aksrt_ticket_urged

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-27 22:17:40 +08:00

259 lines
14 KiB
PHP

<?php
/**
* Registers all non-forum email notifications.
*
* @package AKSRT
*/
namespace AKSRT\Settings;
defined('ABSPATH') || exit;
final class MailNotifications {
public static function init(): void {
add_action('aksrt_order_paid', [self::class, 'order_paid'], 20, 2);
add_action('aksrt_order_status_changed', [self::class, 'order_status'], 20, 3);
add_action('aksrt_asset_transaction_committed', [self::class, 'asset_transaction'], 20, 2);
add_action('aksrt_notification_sent', [self::class, 'notification'], 20, 3);
add_action('aksrt_withdrawal_submitted', [self::class, 'withdrawal_submitted'], 20, 2);
add_action('aksrt_withdrawal_status_changed', [self::class, 'withdrawal_status'], 20, 4);
add_action('aksrt_ban_appeal_submitted', [self::class, 'ban_appeal_submitted'], 20, 2);
add_action('aksrt_ban_appeal_processed', [self::class, 'ban_appeal_processed'], 20, 3);
add_action('aksrt_new_posts_pending', [self::class, 'post_pending'], 20, 1);
add_action('transition_post_status', [self::class, 'post_status_changed'], 20, 3);
add_action('aksrt_report_submitted', [self::class, 'report_submitted'], 20, 2);
add_action('aksrt_report_status_changed', [self::class, 'report_status_changed'], 20, 4);
add_action('aksrt_friend_link_submitted', [self::class, 'friend_link_submitted'], 20, 1);
add_action('aksrt_user_phone_changed', [self::class, 'phone_changed'], 20, 2);
add_action('aksrt_user_restriction_changed', [self::class, 'restriction_changed'], 20, 4);
add_action('aksrt_ticket_urged', [self::class, 'ticket_urged'], 20, 1);
}
/**
* 催单邮件:已指派跟进人时只发给跟进人,否则发给全部管理员(与站内通知同一路由)。
*
* @param array<string,mixed> $ticket
*/
public static function ticket_urged(array $ticket): void {
$vars = [
'username' => (string) ($ticket['user_name'] ?? ''),
'count' => (int) ($ticket['urge_count'] ?? 0),
'ticket_num' => (string) ($ticket['ticket_num'] ?? ''),
'title' => (string) ($ticket['title'] ?? ''),
'url' => admin_url('users.php?page=aksrt_tickets&id=' . (int) ($ticket['id'] ?? 0)),
];
$assignee = get_userdata((int) ($ticket['admin_id'] ?? 0));
if ($assignee instanceof \WP_User && is_email($assignee->user_email)) {
self::send('ticket_urge_admin', $assignee->user_email, $vars);
return;
}
self::send_admin('ticket_urge_admin', $vars);
}
public static function restriction_changed(int $user_id, string $type, string $action, string $reason): void {
$user = get_userdata($user_id);
if (!$user instanceof \WP_User) return;
self::send('user_ban', $user->user_email, [
'username' => $user->display_name,
'status' => $action === 'removed' ? __('限制已解除', 'aksrt') : $type,
'note' => $reason,
]);
}
public static function phone_changed(int $user_id, string $phone): void {
$user = get_userdata($user_id);
if ($user instanceof \WP_User) self::send('phone_changed', $user->user_email, ['username' => $user->display_name, 'site_name' => get_bloginfo('name')]);
}
public static function withdrawal_submitted(int $user_id, array $row): void {
$user = get_userdata($user_id);
if (!$user instanceof \WP_User) return;
$vars = [
'username' => $user->display_name,
'withdrawal_number' => (string) ($row['withdrawal_number'] ?? ''),
'amount' => (string) ($row['gross_amount'] ?? $row['amount'] ?? ''),
'method' => (string) ($row['method'] ?? ''),
];
self::send('withdraw_submitted', $user->user_email, $vars);
self::send_admin('withdraw_submitted_admin', $vars);
}
public static function withdrawal_status(int $user_id, array $row, string $status, string $note): void {
$user = get_userdata($user_id);
if (!$user instanceof \WP_User) return;
self::send('withdraw_completed', $user->user_email, [
'username' => $user->display_name,
'withdrawal_number' => (string) ($row['withdrawal_number'] ?? ''),
'status' => $status,
'amount' => (string) ($row['gross_amount'] ?? $row['amount'] ?? ''),
'note' => $note,
]);
}
public static function order_paid(string $order_number, array $order): void {
$user = get_userdata((int) ($order['user_id'] ?? 0));
if (!$user instanceof \WP_User) return;
$vars = self::order_vars($order_number, $order, $user);
self::send('order_paid', $user->user_email, $vars);
self::send_admin('order_paid_admin', $vars);
}
public static function order_status(string $order_number, string $status, array $order): void {
if ($status === 'paid') return;
$user = get_userdata((int) ($order['user_id'] ?? 0));
if (!$user instanceof \WP_User) return;
self::send('order_status', $user->user_email, self::order_vars($order_number, $order, $user) + ['status' => $status]);
}
public static function asset_transaction($result, $command): void {
if (!$result instanceof \AKSRT\Ledger\LedgerResult || !$command instanceof \AKSRT\Ledger\LedgerCommand || $result->replayed) return;
$reason = $command->reason;
if (!in_array($reason, ['affiliate_commission', 'creator_income', 'transfer', 'tip', 'referral'], true)) return;
foreach ($result->entries as $entry) {
if ((int) ($entry['delta_minor'] ?? 0) <= 0) continue;
$user = get_userdata((int) ($entry['user_id'] ?? 0));
if (!$user instanceof \WP_User) continue;
$amount = self::minor_amount((string) ($entry['asset'] ?? 'balance'), (int) $entry['delta_minor']);
$url = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : home_url('/');
if ($reason === 'affiliate_commission') {
self::send('affiliate_commission', $user->user_email, ['username' => $user->display_name, 'amount' => $amount, 'order_number' => $command->ref_key, 'url' => $url]);
} elseif ($reason === 'creator_income') {
self::send('creator_income', $user->user_email, ['username' => $user->display_name, 'amount' => $amount, 'order_number' => $command->ref_key, 'url' => $url]);
} elseif ($reason === 'referral') {
self::send('referral_reward', $user->user_email, ['username' => $user->display_name, 'amount' => $amount, 'url' => $url]);
} elseif ($reason === 'transfer' && (string) ($entry['entry_type'] ?? '') === 'transfer_in') {
$sender = get_userdata((int) ($entry['counterparty_user_id'] ?? 0));
self::send('transfer_received', $user->user_email, [
'username' => $user->display_name,
'sender' => $sender instanceof \WP_User ? $sender->display_name : __('其他用户', 'aksrt'),
'asset' => ((string) ($entry['asset'] ?? '') === 'points') ? __('积分', 'aksrt') : __('余额', 'aksrt'),
'amount' => $amount,
'note' => (string) ($entry['remark'] ?? ''),
'url' => $url,
]);
}
}
}
public static function notification(int $user_id, string $type, array $data): void {
$user = get_userdata($user_id);
if (!$user instanceof \WP_User) return;
$url = (string) ($data['target_url'] ?? $data['url'] ?? home_url('/'));
$title = (string) ($data['title'] ?? '');
$content = (string) ($data['description'] ?? $data['desc'] ?? '');
$map = [
'comment_reply' => 'comment_reply',
'comment_approved' => 'comment_approved',
];
if (!isset($map[$type])) return;
$event = $map[$type];
$vars = ['username' => $user->display_name, 'site_name' => get_bloginfo('name'), 'title' => $title, 'content' => $content, 'status' => $title, 'note' => $content, 'url' => $url, 'amount' => '', 'withdrawal_number' => ''];
self::send($event, $user->user_email, $vars);
}
public static function auth_submitted(int $user_id, array $application): void {
$user = get_userdata($user_id);
self::send_admin('auth_apply_admin', [
'username' => $user instanceof \WP_User ? $user->display_name : (string) ($application['public_name'] ?? __('用户', 'aksrt')),
'url' => admin_url('users.php?page=aksrt_users_verify'),
]);
}
public static function auth_reviewed(int $user_id, string $status, string $note, int $reviewer_id): void {
$user = get_userdata($user_id);
if ($user instanceof \WP_User) self::send('auth_apply_result', $user->user_email, ['username' => $user->display_name, 'status' => $status, 'note' => $note, 'url' => function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('auth') : home_url('/')]);
}
public static function ban_appeal_submitted(int $user_id, array $payload): void {
$user = get_userdata($user_id);
if ($user instanceof \WP_User) self::send_admin('ban_appeal_admin', ['username' => $user->display_name, 'url' => admin_url('users.php?page=aksrt_user_ban&type=appeal&status=pending')]);
}
public static function ban_appeal_processed(int $user_id, string $decision, string $note): void {
$user = get_userdata($user_id);
if ($user instanceof \WP_User) self::send('user_ban', $user->user_email, ['username' => $user->display_name, 'status' => $decision, 'note' => $note]);
}
public static function post_pending($post): void {
if (!$post instanceof \WP_Post) return;
$author = get_userdata((int) $post->post_author);
if ($author instanceof \WP_User) self::send_admin('post_pending_admin', ['username' => $author->display_name, 'title' => get_the_title($post), 'url' => get_edit_post_link($post->ID, '') ?: admin_url('edit.php')]);
}
public static function post_status_changed(string $new_status, string $old_status, \WP_Post $post): void {
if ($old_status === $new_status || !in_array($new_status, ['publish', 'draft', 'pending'], true)) return;
$author = get_userdata((int) $post->post_author);
if ($author instanceof \WP_User) {
self::send('post_result', $author->user_email, [
'username' => $author->display_name,
'title' => get_the_title($post),
'status' => $new_status,
'url' => get_permalink($post),
]);
}
}
public static function report_submitted(int $report_id, array $report): void {
$reporter = get_userdata((int) ($report['reporter_id'] ?? 0));
self::send_admin('report_admin', ['username' => $reporter instanceof \WP_User ? $reporter->display_name : __('用户', 'aksrt'), 'title' => (string) ($report['target_type'] ?? ''), 'url' => admin_url('users.php?page=aksrt_user_ban')]);
}
public static function report_status_changed(int $report_id, int $reporter_id, string $status, string $note = ''): void {
$reporter = get_userdata($reporter_id);
if (!$reporter instanceof \WP_User) {
return;
}
// 状态值中文化后进入邮件;处理说明来自后台“处理说明”输入(钩子第 4 参)。
$labels = [
'pending' => __('待处理', 'aksrt'),
'reviewing' => __('审核中', 'aksrt'),
'resolved' => __('已解决', 'aksrt'),
'dismissed' => __('已驳回', 'aksrt'),
];
self::send('report_result', $reporter->user_email, [
'username' => $reporter->display_name,
'status' => $labels[$status] ?? $status,
'note' => $note !== '' ? $note : __('—', 'aksrt'),
'url' => home_url('/'),
]);
}
public static function friend_link_submitted(array $link): void {
self::send_admin('links_submit_admin', ['title' => (string) ($link['name'] ?? ''), 'username' => __('访客', 'aksrt'), 'url' => (string) ($link['url'] ?? '')]);
}
/** @param array<string,mixed> $vars */
private static function send(string $event, string $to, array $vars): void {
$placeholders = [];
foreach ($vars as $key => $value) $placeholders['{' . $key . '}'] = (string) $value;
Mail::send_event_email($event, $to, $placeholders);
}
/** @param array<string,mixed> $vars */
private static function send_admin(string $event, array $vars): void {
foreach (self::admin_emails() as $email) self::send($event, $email, $vars);
}
/** @return list<string> */
private static function admin_emails(): array {
$emails = [];
foreach (get_users(['role' => 'administrator', 'fields' => ['user_email'], 'number' => 100]) as $user) {
$email = sanitize_email((string) ($user->user_email ?? ''));
if ($email !== '' && is_email($email)) $emails[$email] = true;
}
return array_keys($emails);
}
/** @return array<string,string> */
private static function order_vars(string $order_number, array $order, \WP_User $user): array {
$product = (string) ($order['product_type'] ?? 'order');
return ['username' => $user->display_name, 'order_number' => $order_number, 'product' => $product, 'amount' => (string) ($order['amount'] ?? ''), 'currency' => (string) ($order['currency'] ?? 'CNY'), 'payment_method' => (string) ($order['payment_method'] ?? ''), 'url' => function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('orders') : home_url('/')];
}
private static function minor_amount(string $asset, int $minor): string {
if ($asset === 'points') return (string) $minor;
return number_format($minor / 100, 2, '.', '');
}
}