diff --git a/assets/admin/src/router/index.js b/assets/admin/src/router/index.js
index 02153ba7..e4e24dcc 100644
--- a/assets/admin/src/router/index.js
+++ b/assets/admin/src/router/index.js
@@ -78,7 +78,6 @@ function createSectionRedirectRoutes() {
*/
const NATIVE_OPS_REDIRECTS = [
{ path: '/users/messages', title: '消息中心', nativeAdminPath: 'users.php?page=aksrt_messages' },
- { path: '/users/referral-codes', title: '推广码运营', nativeAdminPath: 'users.php?page=aksrt_referral_codes' },
{ path: '/users/report', title: '禁封/举报', nativeAdminPath: 'users.php?page=aksrt_user_ban' },
]
diff --git a/src/Admin/Native/UsersMenus.php b/src/Admin/Native/UsersMenus.php
index ab2b0e0c..e2d783a7 100644
--- a/src/Admin/Native/UsersMenus.php
+++ b/src/Admin/Native/UsersMenus.php
@@ -11,7 +11,6 @@ class UsersMenus {
use UsersMenusVerify;
use UsersMenusVip;
- use UsersMenusReferral;
use UsersMenusInviteCode;
use UsersMenusTickets;
@@ -42,15 +41,6 @@ class UsersMenus {
}
public function register_menus(): void {
- add_submenu_page(
- 'users.php',
- __('推广码运营', 'aksrt'),
- __('推广码运营', 'aksrt'),
- 'edit_users',
- 'aksrt_referral_codes',
- [$this, 'render_referral_codes_page']
- );
-
add_submenu_page(
'users.php',
__('邀请码管理', 'aksrt'),
diff --git a/src/Admin/Native/UsersMenusReferral.php b/src/Admin/Native/UsersMenusReferral.php
deleted file mode 100644
index 4e767e49..00000000
--- a/src/Admin/Native/UsersMenusReferral.php
+++ /dev/null
@@ -1,163 +0,0 @@
-
-
-
- 0) {
+ $this->generate_code($invitee_id, false);
+ }
+
if ($invitee_id <= 0 || !$this->feature_enabled()) {
self::$pending_registration_ref = '';
return;
@@ -353,39 +358,6 @@ final class ReferralAttribution {
'callback' => [$this, 'rest_me'],
'permission_callback' => static fn() => is_user_logged_in(),
]);
-
- register_rest_route('aksrt/v1', '/revenue/codes', [
- 'methods' => \WP_REST_Server::READABLE,
- 'callback' => [$this, 'rest_codes'],
- 'permission_callback' => [$this, 'can_manage_codes'],
- 'args' => [
- 'page' => ['type' => 'integer', 'default' => 1, 'minimum' => 1],
- 'per_page' => ['type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 100],
- 'search' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
- 'status' => [
- 'type' => 'string',
- 'default' => 'all',
- 'enum' => ['all', self::STATUS_ACTIVE, self::STATUS_DISABLED, 'missing'],
- ],
- ],
- ]);
-
- foreach (['generate', 'disable', 'regenerate'] as $action) {
- register_rest_route('aksrt/v1', '/revenue/codes/(?P\d+)/' . $action, [
- 'methods' => \WP_REST_Server::CREATABLE,
- 'callback' => function (\WP_REST_Request $request) use ($action) {
- return $this->rest_mutate_code($request, $action);
- },
- 'permission_callback' => [$this, 'can_manage_codes'],
- 'args' => [
- 'user_id' => ['type' => 'integer', 'required' => true, 'minimum' => 1],
- ],
- ]);
- }
- }
-
- public function can_manage_codes(): bool {
- return current_user_can('edit_users');
}
public function rest_me(): \WP_REST_Response {
@@ -418,127 +390,6 @@ final class ReferralAttribution {
], 200);
}
- public function rest_codes(\WP_REST_Request $request): \WP_REST_Response {
- return new \WP_REST_Response($this->list_codes([
- 'page' => (int) $request['page'],
- 'per_page' => (int) $request['per_page'],
- 'search' => (string) $request['search'],
- 'status' => (string) $request['status'],
- ]), 200);
- }
-
- public function rest_mutate_code(\WP_REST_Request $request, string $action) {
- $user_id = (int) $request['user_id'];
- if (!get_userdata($user_id)) {
- return new \WP_Error('aksrt_ref_user_missing', __('用户不存在', 'aksrt'), ['status' => 404]);
- }
-
- $record = $this->code_record($user_id);
- if ($action === 'generate') {
- if ($record['status'] !== 'missing') {
- return new \WP_Error('aksrt_ref_exists', __('该用户已有推广码', 'aksrt'), ['status' => 409]);
- }
- $code = $this->generate_code($user_id, false);
- } elseif ($action === 'disable') {
- if ($record['status'] !== self::STATUS_ACTIVE) {
- return new \WP_Error('aksrt_ref_not_active', __('推广码当前不可禁用', 'aksrt'), ['status' => 409]);
- }
- update_user_meta($user_id, self::META_STATUS, self::STATUS_DISABLED);
- if ($this->code_record($user_id)['status'] !== self::STATUS_DISABLED) {
- return new \WP_Error('aksrt_ref_disable_failed', __('推广码禁用失败', 'aksrt'), ['status' => 500]);
- }
- $code = $record['code'];
- } elseif ($action === 'regenerate') {
- if ($record['status'] === 'missing') {
- return new \WP_Error('aksrt_ref_missing', __('该用户尚未生成推广码', 'aksrt'), ['status' => 409]);
- }
- $code = $this->generate_code($user_id, true);
- } else {
- return new \WP_Error('aksrt_ref_action', __('不支持的推广码操作', 'aksrt'), ['status' => 400]);
- }
-
- if ($code === '') {
- return new \WP_Error('aksrt_ref_generate_failed', __('推广码生成失败', 'aksrt'), ['status' => 500]);
- }
- return rest_ensure_response([
- 'user_id' => $user_id,
- 'ref' => $code,
- 'status' => $action === 'disable' ? self::STATUS_DISABLED : self::STATUS_ACTIVE,
- ]);
- }
-
- private function list_codes(array $args): array {
- global $wpdb;
- $page = max(1, (int) ($args['page'] ?? 1));
- $per_page = max(1, min(100, (int) ($args['per_page'] ?? 20)));
- $status = (string) ($args['status'] ?? 'all');
- $search = trim((string) ($args['search'] ?? ''));
- $where = ['1=1'];
- $params = [];
-
- if ($search !== '') {
- $like = '%' . $wpdb->esc_like($search) . '%';
- $where[] = '(u.user_login LIKE %s OR u.display_name LIKE %s OR u.user_email LIKE %s OR code.meta_value LIKE %s)';
- array_push($params, $like, $like, $like, $like);
- }
- if ($status === 'missing') {
- $where[] = "(code.meta_value IS NULL OR code.meta_value = '')";
- } elseif ($status === self::STATUS_DISABLED) {
- $where[] = "code.meta_value <> '' AND state.meta_value = 'disabled'";
- } elseif ($status === self::STATUS_ACTIVE) {
- $where[] = "code.meta_value <> '' AND (state.meta_value IS NULL OR state.meta_value <> 'disabled')";
- }
-
- $where_sql = implode(' AND ', $where);
- $joins = $wpdb->prepare(
- "LEFT JOIN {$wpdb->usermeta} code ON code.user_id=u.ID AND code.meta_key=%s LEFT JOIN {$wpdb->usermeta} state ON state.user_id=u.ID AND state.meta_key=%s LEFT JOIN {$wpdb->usermeta} created ON created.user_id=u.ID AND created.meta_key=%s",
- self::META_CODE,
- self::META_STATUS,
- self::META_CREATED
- );
- $count_sql = "SELECT COUNT(DISTINCT u.ID) FROM {$wpdb->users} u {$joins} WHERE {$where_sql}";
- $total = (int) ($params
- ? $wpdb->get_var($wpdb->prepare($count_sql, $params))
- : $wpdb->get_var($count_sql));
-
- $relation_table = \AKSRT\Core\DBMigrator::table('affiliate_relation');
- $ledger_table = \AKSRT\Core\DBMigrator::table('commission_ledger');
- $stats_joins = "LEFT JOIN (SELECT promoter_id, COUNT(*) AS bound_count FROM {$relation_table} WHERE status='bound' GROUP BY promoter_id) relation_stats ON relation_stats.promoter_id=u.ID "
- . "LEFT JOIN (SELECT promoter_id, COUNT(DISTINCT source_user_id) AS effective_payers FROM {$ledger_table} WHERE original_amount > 0 GROUP BY promoter_id) payer_stats ON payer_stats.promoter_id=u.ID";
- $list_sql = "SELECT DISTINCT u.ID, u.user_login, u.display_name, u.user_email, code.meta_value AS ref, state.meta_value AS ref_status, created.meta_value AS created_at, "
- . "COALESCE(relation_stats.bound_count, 0) AS bound_count, COALESCE(payer_stats.effective_payers, 0) AS effective_payers "
- . "FROM {$wpdb->users} u {$joins} {$stats_joins} WHERE {$where_sql} ORDER BY u.ID DESC LIMIT %d OFFSET %d";
- $list_params = array_merge($params, [$per_page, ($page - 1) * $per_page]);
- $rows = $wpdb->get_results($wpdb->prepare($list_sql, $list_params), ARRAY_A);
-
- $items = [];
- foreach ((array) $rows as $row) {
- $user_id = (int) $row['ID'];
- $ref = sanitize_key((string) ($row['ref'] ?? ''));
- $items[] = [
- 'user' => [
- 'id' => $user_id,
- 'login' => (string) $row['user_login'],
- 'name' => (string) $row['display_name'],
- 'email' => (string) $row['user_email'],
- ],
- 'ref' => $ref,
- 'status' => $ref === '' ? 'missing' : ((string) ($row['ref_status'] ?? '') === self::STATUS_DISABLED ? self::STATUS_DISABLED : self::STATUS_ACTIVE),
- 'created_at' => (string) ($row['created_at'] ?? ''),
- 'bound_count' => (int) ($row['bound_count'] ?? 0),
- 'effective_payers' => (int) ($row['effective_payers'] ?? 0),
- ];
- }
-
- return [
- 'items' => $items,
- 'total' => $total,
- 'page' => $page,
- 'per_page' => $per_page,
- 'max_pages' => max(1, (int) ceil($total / $per_page)),
- ];
- }
-
private function generate_code(int $user_id, bool $force): string {
if ($user_id <= 0) {
return '';
diff --git a/tests/widget-system/referral-policy-contract.test.cjs b/tests/widget-system/referral-policy-contract.test.cjs
index cef599a9..5eda4202 100644
--- a/tests/widget-system/referral-policy-contract.test.cjs
+++ b/tests/widget-system/referral-policy-contract.test.cjs
@@ -75,3 +75,21 @@ test('lazy settlement backstop guards the earnings entry points', () => {
// 收益总览与我的推广两个入口都接入了兜底。
assert.match(attribution, /CommissionLedger::get_instance\(\)->lazy_settle_due\(\)/)
})
+
+test('referral codes are self-serve: no admin ops page, granted at registration', () => {
+ const service = read('src', 'Payment', 'ReferralAttribution.php')
+ // 管理端代码已清理:无 /revenue/codes 路由、无后台增删改接口、无管理列表。
+ assert.doesNotMatch(service, /revenue\/codes/)
+ assert.doesNotMatch(service, /rest_mutate_code|rest_codes|can_manage_codes|private function list_codes/)
+ // 注册即得唯一推广码:bind_registered_user 内调用 generate_code,不依赖归因开关。
+ assert.match(service, /public function bind_registered_user\(int \$invitee_id\): void \{\s*\n\s*\/\/ 每个新用户注册即拥有唯一推广码[\s\S]*?\$this->generate_code\(\$invitee_id, false\)/)
+
+ // 用户菜单不再挂"推广码运营"页,trait 文件已删除。
+ const menus = read('src', 'Admin', 'Native', 'UsersMenus.php')
+ assert.doesNotMatch(menus, /UsersMenusReferral|aksrt_referral_codes|render_referral_codes_page/)
+ assert.equal(fs.existsSync(path.join(root, 'src', 'Admin', 'Native', 'UsersMenusReferral.php')), false)
+
+ // admin SPA 不再保留跳转路由。
+ const router = read('assets', 'admin', 'src', 'router', 'index.js')
+ assert.doesNotMatch(router, /referral-codes/)
+})