diff --git a/assets/css/routes/user-center/promotion.css b/assets/css/routes/user-center/promotion.css index 1deff321..d80b804d 100644 --- a/assets/css/routes/user-center/promotion.css +++ b/assets/css/routes/user-center/promotion.css @@ -99,8 +99,8 @@ } .uc-content .uc-commission-card { - min-height: 174px; - padding: 24px 26px; + min-height: 116px; + padding: 16px 22px; border: 0; border-radius: 18px; box-shadow: 0 12px 28px rgba(15, 23, 42, .08); @@ -125,7 +125,7 @@ .uc-content .uc-commission-card__body { display: grid; align-content: space-between; - min-height: 126px; + min-height: 84px; } .uc-content .uc-commission-card__body > span, @@ -142,13 +142,13 @@ } .uc-content .uc-commission-card__body strong { - font-size: 42px; + font-size: 32px; line-height: 1; } .uc-content .uc-commission-card__body strong small { margin-left: 4px; - font-size: 18px; + font-size: 15px; } .uc-content .uc-commission-card__body > span:last-child, @@ -174,6 +174,36 @@ backdrop-filter: blur(4px); } +/* 三 Tab 推广收益:佣金详细页采用紧凑的一列信息清单。 */ +.uc-content .uc-reward-area { display: grid; gap: 18px; padding: 24px 28px 30px; border-radius: 18px; background: var(--surface-1, #fff); box-shadow: 0 8px 26px rgba(15, 23, 42, .06); } +.uc-content .uc-reward-area__heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; } +.uc-content .uc-reward-area__heading h3 { margin: 0; font-size: 20px; } +.uc-content .uc-reward-area__heading > span { color: var(--color-ink-4); font-size: 13px; } +.uc-content .uc-reward-tabs { display: flex; gap: 24px; border-bottom: 1px solid var(--color-line-soft, rgba(15, 23, 42, .08)); } +.uc-content .uc-reward-tabs a { position: relative; padding: 0 0 12px; color: var(--color-ink-4); font-size: 14px; text-decoration: none; } +.uc-content .uc-reward-tabs a.is-active { color: var(--color-primary); font-weight: 700; } +.uc-content .uc-reward-tabs a.is-active::after { position: absolute; right: 0; bottom: -1px; left: 0; height: 2px; border-radius: 99px; background: var(--color-primary); content: ""; } +.uc-content .uc-promotion-summary-list { display: grid; gap: 0; max-width: 920px; } +.uc-content .uc-promotion-summary-row { display: grid; grid-template-columns: 112px minmax(0, 1fr) auto; align-items: center; gap: 12px; min-height: 54px; padding: 8px 0; border-bottom: 1px solid var(--color-line-soft, rgba(15, 23, 42, .07)); } +.uc-content .uc-promotion-summary-row > span { color: var(--color-ink-3); font-size: 14px; } +.uc-content .uc-promotion-summary-row > strong { color: var(--color-ink); font-size: 16px; } +.uc-content .uc-promotion-summary-row > em { color: var(--color-ink-4); font-style: normal; } +.uc-content .uc-promotion-summary-row--link input { width: 100%; min-width: 0; height: 38px; padding: 0 10px; border: 1px solid var(--color-line-soft, rgba(15, 23, 42, .12)); border-radius: 7px; color: var(--color-primary); background: var(--surface-2, #f8fafc); } +.uc-content .uc-promotion-summary-row--link button { min-width: 64px; height: 38px; padding: 0 12px; border-radius: 7px; } +.uc-content .uc-promotion-summary-row--link small { grid-column: 2 / -1; min-height: 16px; color: var(--color-success, #16a34a); font-size: 12px; } +.uc-content .uc-promotion-summary-row--link small.is-error { color: var(--color-error, #dc2626); } +.uc-content .uc-promotion-summary-row--total > div { display: flex; flex-wrap: wrap; gap: 12px; } +.uc-content .uc-promotion-summary-row--total strong { padding: 7px 11px; border-radius: 7px; color: var(--color-primary); background: var(--surface-2, #f8fafc); font-size: 14px; } +.uc-content .uc-promotion-summary-row--explain { align-items: start; padding-top: 16px; border-bottom: 0; } +.uc-content .uc-promotion-summary-row--explain .uc-reward-richtext { grid-column: 2 / -1; color: var(--color-ink-3); font-size: 14px; line-height: 1.75; } +.uc-content .uc-promotion-summary-row--explain .uc-reward-richtext p { margin: 0; } +.uc-content .uc-promotion-user-list { display: grid; } +.uc-content .uc-promotion-user-row { display: flex; align-items: center; gap: 12px; padding: 13px 0; border-bottom: 1px solid var(--color-line-soft, rgba(15, 23, 42, .07)); } +.uc-content .uc-promotion-user-row img { width: 38px; height: 38px; border-radius: 50%; object-fit: cover; background: var(--surface-2); } +.uc-content .uc-promotion-user-row > div { display: grid; flex: 1; gap: 3px; min-width: 0; } +.uc-content .uc-promotion-user-row small { color: var(--color-ink-4); font-size: 12px; } +.uc-content .uc-promotion-user-row > span { color: var(--color-success, #16a34a); font-size: 12px; } + .uc-content .uc-commission-card__action:hover { background: rgba(255, 255, 255, .24); } @@ -293,16 +323,16 @@ @media (max-width: 560px) { .uc-content .uc-commission-card { - min-height: 150px; + min-height: 100px; padding: 20px; } .uc-content .uc-commission-card__body { - min-height: 108px; + min-height: 72px; } .uc-content .uc-commission-card__body strong { - font-size: 36px; + font-size: 28px; } .uc-content .uc-affiliate-link-card { @@ -321,4 +351,14 @@ .uc-content .uc-affiliate-stats { grid-template-columns: 1fr; } + + .uc-content .uc-reward-area { padding: 20px 16px 24px; } + .uc-content .uc-reward-area__heading { display: grid; gap: 4px; } + .uc-content .uc-reward-tabs { gap: 16px; overflow-x: auto; } + .uc-content .uc-promotion-summary-row { grid-template-columns: 92px minmax(0, 1fr) auto; gap: 8px; } + .uc-content .uc-promotion-summary-row--link { grid-template-columns: 1fr auto; } + .uc-content .uc-promotion-summary-row--link > span { grid-column: 1 / -1; } + .uc-content .uc-promotion-summary-row--link small { grid-column: 1 / -1; } + .uc-content .uc-promotion-summary-row--explain { grid-template-columns: 1fr; } + .uc-content .uc-promotion-summary-row--explain .uc-reward-richtext { grid-column: 1; } } diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php index c29a77d7..7e677217 100644 --- a/inc/helpers/aksrt-pay.php +++ b/inc/helpers/aksrt-pay.php @@ -599,10 +599,9 @@ if (!function_exists('aksrt_get_post_rebate_discount')) { return 0.0; } $uid = $user_id > 0 ? $user_id : (function_exists('get_current_user_id') ? (int) get_current_user_id() : 0); - $referrer_id = 0; - if (class_exists('\AKSRT\Credits\Invite') && method_exists(\AKSRT\Credits\Invite::get_instance(), 'get_referrer')) { - $referrer_id = (int) \AKSRT\Credits\Invite::get_instance()->get_referrer($uid); - } + $referrer_id = class_exists('\AKSRT\Payment\ReferralAttribution') + ? \AKSRT\Payment\ReferralAttribution::get_instance()->resolve($uid) + : 0; if ($referrer_id <= 0) { return 0.0; } diff --git a/inc/helpers/user-center-renderers/profile-revenue.php b/inc/helpers/user-center-renderers/profile-revenue.php index 0c31f62e..223c436c 100644 --- a/inc/helpers/user-center-renderers/profile-revenue.php +++ b/inc/helpers/user-center-renderers/profile-revenue.php @@ -46,7 +46,7 @@ if ($details_text === '') { $revenue_request = is_array($context['revenue_request'] ?? null) ? $context['revenue_request'] : []; $active_reward_tab = sanitize_key((string) ($revenue_request['reward_tab'] ?? ($_GET['reward_tab'] ?? 'detail'))); -if (!in_array($active_reward_tab, ['detail', 'orders'], true)) { +if (!in_array($active_reward_tab, ['detail', 'orders', 'users'], true)) { $active_reward_tab = 'detail'; } $base_url = function_exists('aksrt_get_user_center_tab_url') @@ -67,6 +67,15 @@ $commission_view = class_exists('\AKSRT\Payment\CommissionLedger') ]) : ['items' => [], 'total' => 0, 'max_pages' => 1, 'page' => 1]; $commission_items = (array) ($commission_view['items'] ?? []); +$user_page = max(1, (int) ($_GET['user_page'] ?? 1)); +$user_view = class_exists('\AKSRT\Payment\ReferralAttribution') + ? \AKSRT\Payment\ReferralAttribution::get_instance()->list_referred_users($user_id, $user_page, 10) + : ['items' => [], 'total' => 0, 'max_pages' => 1, 'page' => 1]; +$user_total_pages = max(1, (int) ($user_view['max_pages'] ?? 1)); +if ($user_page > $user_total_pages && class_exists('\AKSRT\Payment\ReferralAttribution')) { + $user_page = $user_total_pages; + $user_view = \AKSRT\Payment\ReferralAttribution::get_instance()->list_referred_users($user_id, $user_page, 10); +} $commission_total_pages = max(1, (int) ($commission_view['max_pages'] ?? ceil(max(1, (int) ($commission_view['total'] ?? 0)) / 10))); if ($commission_page > $commission_total_pages) { $commission_page = $commission_total_pages; @@ -104,44 +113,31 @@ $status_labels = [ -
-
-

-

-
- -
-
—
-
—
-
—
-
-

-
-

- +

+
-
-
-

-
-
+
+
+ +
%
+
—
+
累计元已提现元
+
+
@@ -160,10 +156,19 @@ $status_labels = [

-

+
+

+ +
+ +
+ +
+ 1) : ?> +

+
-
get_referrer($user_id); - } + $referrer_id = class_exists('\AKSRT\Payment\ReferralAttribution') + ? \AKSRT\Payment\ReferralAttribution::get_instance()->resolve($user_id) + : 0; if ($referrer_id <= 0) { return $empty; } diff --git a/src/Credits/Invite.php b/src/Credits/Invite.php index 1d022f4e..f66f7405 100644 --- a/src/Credits/Invite.php +++ b/src/Credits/Invite.php @@ -74,10 +74,10 @@ class Invite { ], ]); } - /** Capture ?ref=XYZ, with legacy ?invite=XYZ support. */ + /** Capture the unified ?ref=XYZ referral parameter. */ public function capture_invite_cookie(): void { if (is_admin()) return; - $raw_code = $_GET['ref'] ?? $_GET['invite'] ?? ''; + $raw_code = $_GET['ref'] ?? ''; if ($raw_code === '') return; // 邀请注册功能关闭时不处理 if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::invite_registration_enabled()) return; diff --git a/src/Payment/CommissionLedger.php b/src/Payment/CommissionLedger.php index 755bb111..99adcffa 100644 --- a/src/Payment/CommissionLedger.php +++ b/src/Payment/CommissionLedger.php @@ -90,7 +90,15 @@ final class CommissionLedger { public function overview(int $user_id): array { global $wpdb; $table=$this->table('commission_ledger'); $row=$wpdb->get_row($wpdb->prepare("SELECT COALESCE(SUM(original_amount+adjustment_total),0) total, COALESCE(SUM(CASE WHEN status='pending' THEN original_amount+adjustment_total ELSE 0 END),0) pending, COALESCE(SUM(CASE WHEN status='settled' THEN original_amount+adjustment_total ELSE 0 END),0) settled FROM {$table} WHERE promoter_id=%d",$user_id),ARRAY_A) ?: []; - return ['total'=>round((float)($row['total']??0),2),'pending'=>round((float)($row['pending']??0),2),'settled'=>round((float)($row['settled']??0),2),'wallet'=>class_exists('\\AKSRT\\Credits\\Balance')?\AKSRT\Credits\Balance::balance($user_id):0.0,'debt'=>round((float)get_user_meta($user_id,self::DEBT_META,true),2)]; + $withdrawn = 0.0; + $withdrawal_table = $this->table('withdrawal'); + if ($withdrawal_table !== '') { + $withdrawn = (float) $wpdb->get_var($wpdb->prepare( + "SELECT COALESCE(SUM(CASE WHEN status='completed' THEN CASE WHEN gross_amount > 0 THEN gross_amount ELSE amount END ELSE 0 END),0) FROM {$withdrawal_table} WHERE user_id=%d", + $user_id + )); + } + return ['total'=>round((float)($row['total']??0),2),'pending'=>round((float)($row['pending']??0),2),'settled'=>round((float)($row['settled']??0),2),'wallet'=>class_exists('\\AKSRT\\Credits\\Balance')?\AKSRT\Credits\Balance::balance($user_id):0.0,'withdrawn'=>round($withdrawn,2),'debt'=>round((float)get_user_meta($user_id,self::DEBT_META,true),2)]; } public function list_entries(int $user_id, array $args=[]): array { global $wpdb; $table=$this->table('commission_ledger'); $limit=max(1,min(100,(int)($args['per_page']??20))); $page=max(1,(int)($args['page']??1)); $offset=($page-1)*$limit; $where='promoter_id=%d'; $params=[$user_id]; $status=sanitize_key((string)($args['status']??'')); if(in_array($status,['pending','settled','cancelled'],true)){ $where.=' AND status=%s'; $params[]=$status; } $total=(int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE {$where}",$params)); $rows=$wpdb->get_results($wpdb->prepare("SELECT * FROM {$table} WHERE {$where} ORDER BY id DESC LIMIT %d OFFSET %d",array_merge($params,[$limit,$offset])),ARRAY_A); return ['items'=>$rows?:[],'total'=>$total,'page'=>$page,'per_page'=>$limit,'max_pages'=>max(1,(int)ceil($total/$limit))]; diff --git a/src/Payment/ReferralAttribution.php b/src/Payment/ReferralAttribution.php index bd5a4ab7..91bdb78c 100644 --- a/src/Payment/ReferralAttribution.php +++ b/src/Payment/ReferralAttribution.php @@ -140,6 +140,47 @@ final class ReferralAttribution { return ['promoters' => $promoters, 'effective_payers' => $payers]; } + /** + * Return users bound through the forward-only ref relation. + * + * @return array{items:array>,total:int,page:int,per_page:int,max_pages:int} + */ + public function list_referred_users(int $promoter_id, int $page = 1, int $per_page = 10): array { + global $wpdb; + $table = \AKSRT\Core\DBMigrator::table('affiliate_relation'); + $page = max(1, $page); + $per_page = max(1, min(50, $per_page)); + if ($promoter_id <= 0 || $table === '') { + return ['items' => [], 'total' => 0, 'page' => $page, 'per_page' => $per_page, 'max_pages' => 1]; + } + $total = (int) $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM {$table} WHERE promoter_id=%d AND status='bound' AND invitee_id > 0", + $promoter_id + )); + $rows = $wpdb->get_results($wpdb->prepare( + "SELECT invitee_id, referral_code, bound_at, status FROM {$table} WHERE promoter_id=%d AND status='bound' AND invitee_id > 0 ORDER BY id DESC LIMIT %d OFFSET %d", + $promoter_id, + $per_page, + ($page - 1) * $per_page + ), ARRAY_A); + $items = []; + foreach ((array) $rows as $row) { + $invitee_id = (int) ($row['invitee_id'] ?? 0); + $user = $invitee_id > 0 ? get_userdata($invitee_id) : null; + if (!$user instanceof \WP_User) { + continue; + } + $items[] = [ + 'user_id' => $invitee_id, + 'name' => $user->display_name, + 'avatar' => get_avatar_url($invitee_id, ['size' => 64]), + 'bound_at' => (string) ($row['bound_at'] ?? ''), + 'status' => (string) ($row['status'] ?? 'bound'), + ]; + } + return ['items' => $items, 'total' => $total, 'page' => $page, 'per_page' => $per_page, 'max_pages' => max(1, (int) ceil($total / $per_page))]; + } + public function register_routes(): void { register_rest_route('aksrt/v1', '/promotion/me', [ 'methods' => \WP_REST_Server::READABLE,