feat(invite-code): admin page rebuilt as server-rendered three-view ops UI
对齐参考主题的邀请码运营页 UI/UX(布局与交互模式为自研实现): - 三视图切换:列表 / 添加邀请码 / 导出邀请码(page-title-action 按钮) - 列表:subsubsub 状态筛选(全部/未使用/已使用)、关键字搜索、批量删除、 可排序表头(邀请码/创建时间/更新时间/状态/标识,orderby 箭头)、 点击复制邀请码 + 复制成功浮层、标识点击定位、分页条 - 状态列:未使用(蓝)/已使用(红)+使用者链接,复用使用记录表 - 添加视图:添加方式按钮组(系统自动生成/导入邀请码)、生成数量(默认20/上限1000)、 标识(默认 invita_YmdHis)、使用奖励字段集(经验值/积分/余额/VIP+赠送时长依赖显示)、 高级选项折叠(自定义位数)、导入区格式说明与数据示例 - 导出视图:状态选择 + 文本文档/Excel表格 双格式 + 文本分隔符 - 传输层从 fetch+JSON 改为 admin_post 表单提交 + 回跳提示; 管理端 REST 全部移除(保留公开 verify),死代码清理 - 建表新增 updated_at(消费/生成/导入维护),列表'更新时间'列与默认排序依据 - 契约测试 357→358 项固化新架构
This commit is contained in:
1 parent
a5f0484466
commit
8d9cca2fe5
5 files changed
+605
-431
No files matched your search
+217
-155
@@ -31,6 +31,9 @@ final class InviteCode {
|
||||
add_action('user_register', [$this, 'handle_registration'], 30, 1);
|
||||
add_action('aksrt_email_verified', [$this, 'handle_email_verified'], 10, 1);
|
||||
add_action('rest_api_init', [$this, 'register_routes']);
|
||||
add_action('admin_post_aksrt_invite_add', [$this, 'handle_add']);
|
||||
add_action('admin_post_aksrt_invite_delete', [$this, 'handle_delete']);
|
||||
add_action('admin_post_aksrt_invite_export', [$this, 'handle_export']);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
@@ -133,10 +136,12 @@ final class InviteCode {
|
||||
if ($code === '' || $table === '') {
|
||||
return null;
|
||||
}
|
||||
$now = current_time('mysql');
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$table} SET used_count = used_count + 1 WHERE code = %s AND status = 'active' AND (max_uses = 0 OR used_count < max_uses) AND (expires_at IS NULL OR expires_at > %s)",
|
||||
"UPDATE {$table} SET used_count = used_count + 1, updated_at = %s WHERE code = %s AND status = 'active' AND (max_uses = 0 OR used_count < max_uses) AND (expires_at IS NULL OR expires_at > %s)",
|
||||
$now,
|
||||
$code,
|
||||
current_time('mysql')
|
||||
$now
|
||||
));
|
||||
if ($updated !== 1) {
|
||||
return null;
|
||||
@@ -232,9 +237,10 @@ final class InviteCode {
|
||||
if ($table === '') {
|
||||
return new \WP_Error('aksrt_invite_db', __('数据表不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$count = min(500, max(1, (int) ($args['count'] ?? 1)));
|
||||
$count = min(1000, max(1, (int) ($args['count'] ?? 1)));
|
||||
$max_uses = max(0, (int) ($args['max_uses'] ?? 1));
|
||||
$expires_days = max(0, (int) ($args['expires_days'] ?? 0));
|
||||
$code_length = min(50, max(4, (int) ($args['code_length'] ?? self::CODE_LENGTH)));
|
||||
$rewards = $this->normalize_rewards((string) wp_json_encode($args['rewards'] ?? []));
|
||||
if (max($rewards['points'], $rewards['balance_minor'], $rewards['vip_level'] * $rewards['vip_days'], $rewards['level_points']) === 0) {
|
||||
$rewards = ['points' => 0, 'balance_minor' => 0, 'vip_level' => 0, 'vip_days' => 0, 'level_points' => 0];
|
||||
@@ -251,7 +257,7 @@ final class InviteCode {
|
||||
$code = '';
|
||||
for ($attempt = 0; $attempt < 20; $attempt++) {
|
||||
$candidate = '';
|
||||
for ($j = 0; $j < self::CODE_LENGTH; $j++) {
|
||||
for ($j = 0; $j < $code_length; $j++) {
|
||||
$candidate .= self::ALPHABET[random_int(0, strlen(self::ALPHABET) - 1)];
|
||||
}
|
||||
$exists = $wpdb->get_var($wpdb->prepare("SELECT id FROM {$table} WHERE code = %s", $candidate));
|
||||
@@ -273,7 +279,8 @@ final class InviteCode {
|
||||
'expires_at' => $expires_at,
|
||||
'created_by' => $created_by,
|
||||
'created_at' => $created_at,
|
||||
], ['%s', '%s', '%s', '%s', '%d', '%s', '%s', '%d', '%s']);
|
||||
'updated_at' => $created_at,
|
||||
], ['%s', '%s', '%s', '%s', '%d', '%s', '%s', '%d', '%s', '%s']);
|
||||
if ($inserted === 1) {
|
||||
$codes[] = $code;
|
||||
}
|
||||
@@ -342,7 +349,8 @@ final class InviteCode {
|
||||
'expires_at' => $expires_at,
|
||||
'created_by' => $created_by,
|
||||
'created_at' => $created_at,
|
||||
], ['%s', '%s', '%s', '%s', '%d', '%s', '%s', '%d', '%s']);
|
||||
'updated_at' => $created_at,
|
||||
], ['%s', '%s', '%s', '%s', '%d', '%s', '%s', '%d', '%s', '%s']);
|
||||
if ($inserted === 1) {
|
||||
$imported++;
|
||||
} else {
|
||||
@@ -394,45 +402,77 @@ final class InviteCode {
|
||||
return $parts === [] ? '无' : implode('&', $parts);
|
||||
}
|
||||
|
||||
public function set_status(int $id, string $status): bool {
|
||||
global $wpdb;
|
||||
$table = $this->table();
|
||||
if ($id <= 0 || $table === '' || !in_array($status, ['active', 'disabled'], true)) {
|
||||
return false;
|
||||
}
|
||||
return $wpdb->update($table, ['status' => $status], ['id' => $id], ['%s'], ['%d']) === 1;
|
||||
/** 状态过滤片段(列表与导出共用):all=全部,unused=未使用,used=已使用。 */
|
||||
private function status_where(string $status): array {
|
||||
return match ($status) {
|
||||
'unused' => ['used_count = 0', []],
|
||||
'used' => ['used_count > 0', []],
|
||||
default => ['1=1', []],
|
||||
};
|
||||
}
|
||||
|
||||
/** @return array{items:array<int,array<string,mixed>>,total:int,page:int,per_page:int,max_pages:int} */
|
||||
/**
|
||||
* 删除邀请码(连同其使用记录)。
|
||||
*
|
||||
* @param array<int,int> $ids
|
||||
*/
|
||||
public function delete_codes(array $ids): int {
|
||||
global $wpdb;
|
||||
$table = $this->table();
|
||||
$usage_table = \AKSRT\Core\DBMigrator::table('invite_code_usage');
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), static fn($id) => $id > 0)));
|
||||
if ($ids === [] || $table === '') {
|
||||
return 0;
|
||||
}
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
|
||||
if ($usage_table !== '') {
|
||||
$wpdb->query($wpdb->prepare("DELETE FROM {$usage_table} WHERE code_id IN ({$placeholders})", $ids));
|
||||
}
|
||||
$wpdb->query($wpdb->prepare("DELETE FROM {$table} WHERE id IN ({$placeholders})", $ids));
|
||||
return (int) $wpdb->rows_affected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务端列表查询:状态筛选 / 关键字搜索 / 标识定位 / 可排序 / 分页。
|
||||
*
|
||||
* @return array{items:array<int,array<string,mixed>>,total:int,counts:array{all:int,unused:int,used:int},page:int,per_page:int,max_pages:int,orderby:string,desc:string}
|
||||
*/
|
||||
public function list_codes(array $args): array {
|
||||
global $wpdb;
|
||||
$table = $this->table();
|
||||
$page = max(1, (int) ($args['page'] ?? 1));
|
||||
$per_page = max(1, min(100, (int) ($args['per_page'] ?? 20)));
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
$search = trim((string) ($args['search'] ?? ''));
|
||||
$per_page = max(1, min(100, (int) ($args['per_page'] ?? 30)));
|
||||
$status = in_array(($args['status'] ?? 'all'), ['all', 'unused', 'used'], true) ? (string) $args['status'] : 'all';
|
||||
$search = trim((string) ($args['s'] ?? ''));
|
||||
$remark = trim((string) ($args['remark'] ?? ''));
|
||||
$orderby_map = ['code' => 'code', 'created_at' => 'created_at', 'updated_at' => 'updated_at', 'used_count' => 'used_count', 'remark' => 'remark'];
|
||||
$orderby = $orderby_map[sanitize_key((string) ($args['orderby'] ?? ''))] ?? 'updated_at';
|
||||
$desc = strtoupper((string) ($args['desc'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC';
|
||||
if ($table === '') {
|
||||
return ['items' => [], 'total' => 0, 'page' => $page, 'per_page' => $per_page, 'max_pages' => 1];
|
||||
return ['items' => [], 'total' => 0, 'counts' => ['all' => 0, 'unused' => 0, 'used' => 0], 'page' => $page, 'per_page' => $per_page, 'max_pages' => 1, 'orderby' => $orderby, 'desc' => $desc];
|
||||
}
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
if (in_array($status, ['active', 'disabled'], true)) {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
[$status_sql, $status_params] = $this->status_where($status);
|
||||
$where = [$status_sql];
|
||||
$params = $status_params;
|
||||
if ($search !== '') {
|
||||
$where[] = '(code LIKE %s OR batch_id LIKE %s)';
|
||||
$like = '%' . $wpdb->esc_like($search) . '%';
|
||||
array_push($params, $like, $like);
|
||||
$where[] = '(code LIKE %s OR remark LIKE %s OR batch_id LIKE %s)';
|
||||
array_push($params, $like, $like, $like);
|
||||
}
|
||||
if ($remark !== '') {
|
||||
$where[] = 'remark = %s';
|
||||
$params[] = $remark;
|
||||
}
|
||||
$where_sql = implode(' AND ', $where);
|
||||
|
||||
$total = (int) ($params
|
||||
? $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE {$where_sql}", $params))
|
||||
: $wpdb->get_var("SELECT COUNT(*) FROM {$table}"));
|
||||
$count_row = $params
|
||||
? $wpdb->get_row($wpdb->prepare("SELECT COUNT(*) AS all_count, COALESCE(SUM(used_count = 0), 0) AS unused_count, COALESCE(SUM(used_count > 0), 0) AS used_count FROM {$table} WHERE {$where_sql}", $params), ARRAY_A)
|
||||
: $wpdb->get_row("SELECT COUNT(*) AS all_count, COALESCE(SUM(used_count = 0), 0) AS unused_count, COALESCE(SUM(used_count > 0), 0) AS used_count FROM {$table}", ARRAY_A);
|
||||
$total = (int) ($count_row['all_count'] ?? 0);
|
||||
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE {$where_sql} ORDER BY id DESC LIMIT %d OFFSET %d",
|
||||
"SELECT * FROM {$table} WHERE {$where_sql} ORDER BY {$orderby} {$desc}, id DESC LIMIT %d OFFSET %d",
|
||||
array_merge($params, [$per_page, ($page - 1) * $per_page])
|
||||
), ARRAY_A);
|
||||
|
||||
@@ -440,13 +480,13 @@ final class InviteCode {
|
||||
$code_ids = array_map(static fn($row) => (int) $row['id'], (array) $rows);
|
||||
$recent_users = $this->recent_users_map($code_ids);
|
||||
foreach ((array) $rows as $row) {
|
||||
$rewards = $this->normalize_rewards((string) ($row['rewards'] ?? ''));
|
||||
$items[] = [
|
||||
'id' => (int) $row['id'],
|
||||
'code' => (string) $row['code'],
|
||||
'batch_id' => (string) $row['batch_id'],
|
||||
'remark' => (string) ($row['remark'] ?? ''),
|
||||
'rewards' => $rewards,
|
||||
'rewards' => (string) ($row['rewards'] ?? ''),
|
||||
'reward_text' => $this->reward_text((string) ($row['rewards'] ?? '')),
|
||||
'max_uses' => (int) $row['max_uses'],
|
||||
'used_count' => (int) $row['used_count'],
|
||||
'recent_users' => $recent_users[(int) $row['id']] ?? [],
|
||||
@@ -454,18 +494,45 @@ final class InviteCode {
|
||||
'expired' => !empty($row['expires_at']) && strtotime((string) $row['expires_at']) <= time(),
|
||||
'expires_at' => (string) ($row['expires_at'] ?? ''),
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'updated_at' => (string) ($row['updated_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'counts' => [
|
||||
'all' => (int) ($count_row['all_count'] ?? 0),
|
||||
'unused' => (int) ($count_row['unused_count'] ?? 0),
|
||||
'used' => (int) ($count_row['used_count'] ?? 0),
|
||||
],
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'max_pages' => max(1, (int) ceil($total / $per_page)),
|
||||
'orderby' => $orderby,
|
||||
'desc' => $desc,
|
||||
];
|
||||
}
|
||||
|
||||
/** 奖励 JSON → 多行展示文本(对应导入参数串的可读形态)。 */
|
||||
public function reward_text(string $rewards_json): string {
|
||||
$rewards = $this->normalize_rewards($rewards_json);
|
||||
$lines = [];
|
||||
if ($rewards['level_points'] > 0) {
|
||||
$lines[] = sprintf(__('经验值+%d', 'aksrt'), $rewards['level_points']);
|
||||
}
|
||||
if ($rewards['points'] > 0) {
|
||||
$lines[] = sprintf(__('积分+%d', 'aksrt'), $rewards['points']);
|
||||
}
|
||||
if ($rewards['balance_minor'] > 0) {
|
||||
$lines[] = sprintf(__('余额+%s', 'aksrt'), number_format($rewards['balance_minor'] / 100, 2));
|
||||
}
|
||||
if ($rewards['vip_level'] > 0 && $rewards['vip_days'] > 0) {
|
||||
$lines[] = sprintf(__('VIP%d×%d天', 'aksrt'), $rewards['vip_level'], $rewards['vip_days']);
|
||||
}
|
||||
return $lines === [] ? __('无', 'aksrt') : implode('<br>', $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量取每个码最近的使用者(最多 3 个),用于运营列表展示。
|
||||
*
|
||||
@@ -506,7 +573,7 @@ final class InviteCode {
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* REST
|
||||
* REST(仅公开校验)与管理表单处理器
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
public function register_routes(): void {
|
||||
@@ -521,143 +588,138 @@ final class InviteCode {
|
||||
'code' => ['type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_key'],
|
||||
],
|
||||
]);
|
||||
|
||||
$admin_guard = fn() => current_user_can('edit_users')
|
||||
? true
|
||||
: new \WP_Error('aksrt_invite_forbidden', __('无权执行此操作', 'aksrt'), ['status' => 403]);
|
||||
$write_guard = function () use ($admin_guard) {
|
||||
$allowed = $admin_guard();
|
||||
if (is_wp_error($allowed)) {
|
||||
return $allowed;
|
||||
}
|
||||
$nonce = (string) isset($_SERVER['HTTP_X_WP_NONCE']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_X_WP_NONCE'])) : '';
|
||||
return $nonce !== '' && wp_verify_nonce($nonce, 'wp_rest')
|
||||
? true
|
||||
: new \WP_Error('aksrt_rest_nonce', __('安全验证失败,请刷新页面后重试', 'aksrt'), ['status' => 403]);
|
||||
};
|
||||
|
||||
register_rest_route('aksrt/v1', '/invite-codes', [
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => fn(\WP_REST_Request $request) => rest_ensure_response($this->list_codes($request->get_params())),
|
||||
'permission_callback' => $admin_guard,
|
||||
'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' => '', 'enum' => ['', 'active', 'disabled']],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/invite-codes/generate', [
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => fn(\WP_REST_Request $request) => $this->rest_generate($request),
|
||||
'permission_callback' => $write_guard,
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/invite-codes/import', [
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => function (\WP_REST_Request $request) {
|
||||
$result = $this->import_codes([
|
||||
'data' => (string) $request->get_param('data'),
|
||||
'division' => (string) $request->get_param('division'),
|
||||
'max_uses' => (int) $request->get_param('max_uses'),
|
||||
'expires_days' => (int) $request->get_param('expires_days'),
|
||||
]);
|
||||
return is_wp_error($result) ? $result : new \WP_REST_Response(['success' => true] + $result, 201);
|
||||
},
|
||||
'permission_callback' => $write_guard,
|
||||
'args' => [
|
||||
'data' => ['type' => 'string', 'required' => true],
|
||||
'division' => ['type' => 'string', 'default' => ' ', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'max_uses' => ['type' => 'integer', 'default' => 1, 'minimum' => 0, 'maximum' => 9999],
|
||||
'expires_days' => ['type' => 'integer', 'default' => 0, 'minimum' => 0, 'maximum' => 3650],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/invite-codes/export', [
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'rest_export'],
|
||||
'permission_callback' => $admin_guard,
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/invite-codes/export-text', [
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'rest_export_text'],
|
||||
'permission_callback' => $admin_guard,
|
||||
'args' => [
|
||||
'division' => ['type' => 'string', 'default' => ' ', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
foreach (['disable' => 'disabled', 'enable' => 'active'] as $action => $status) {
|
||||
register_rest_route('aksrt/v1', '/invite-codes/(?P<id>\d+)/' . $action, [
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => function (\WP_REST_Request $request) use ($status) {
|
||||
$updated = $this->set_status((int) $request['id'], $status);
|
||||
return $updated
|
||||
? rest_ensure_response(['success' => true, 'id' => (int) $request['id'], 'status' => $status])
|
||||
: new \WP_Error('aksrt_invite_status', __('邀请码状态更新失败', 'aksrt'), ['status' => 409]);
|
||||
},
|
||||
'permission_callback' => $write_guard,
|
||||
'args' => ['id' => ['type' => 'integer', 'minimum' => 1, 'required' => true]],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function rest_generate(\WP_REST_Request $request) {
|
||||
$rewards = $request->get_param('rewards');
|
||||
/** 管理操作入口守卫:权限 + nonce,通过后回跳带结果的列表页。 */
|
||||
private function guard_and_redirect(string $tab = ''): string {
|
||||
if (!current_user_can('edit_users')) {
|
||||
wp_die(esc_html__('您没有权限执行此操作。', 'aksrt'), '', ['response' => 403]);
|
||||
}
|
||||
check_admin_referer('aksrt_invite_admin', 'aksrt_nonce');
|
||||
return admin_url('users.php?page=aksrt_invite_codes' . ($tab !== '' ? '&tab=' . $tab : ''));
|
||||
}
|
||||
|
||||
private function redirect_with_notice(string $url, string $title, string $message, bool $is_error = false): never {
|
||||
wp_safe_redirect(add_query_arg([
|
||||
'aksrt_inv_title' => rawurlencode($title),
|
||||
'aksrt_inv_msg' => rawurlencode($message),
|
||||
'aksrt_inv_type' => $is_error ? 'error' : 'success',
|
||||
], $url));
|
||||
exit;
|
||||
}
|
||||
|
||||
/** 添加视图提交:系统自动生成 或 文本导入。 */
|
||||
public function handle_add(): void {
|
||||
$back = $this->guard_and_redirect('add');
|
||||
$add_type = ($_POST['add_type'] ?? 'auto') === 'import' ? 'import' : 'auto';
|
||||
|
||||
if ($add_type === 'import') {
|
||||
$data = (string) wp_unslash($_POST['import_data'] ?? '');
|
||||
if (trim($data) === '') {
|
||||
$this->redirect_with_notice($back, __('错误!', 'aksrt'), __('请粘贴您需要导入的数据', 'aksrt'), true);
|
||||
}
|
||||
$division = trim((string) wp_unslash($_POST['import_division'] ?? ' '));
|
||||
$result = $this->import_codes(['data' => $data, 'division' => $division]);
|
||||
if (is_wp_error($result)) {
|
||||
$this->redirect_with_notice($back, __('错误!', 'aksrt'), $result->get_error_message(), true);
|
||||
}
|
||||
$message = sprintf(__('成功导入%d个邀请码', 'aksrt'), $result['imported'])
|
||||
. ($result['failed'] ? sprintf(__(',%d个导入失败', 'aksrt'), $result['failed']) : '');
|
||||
$this->redirect_with_notice($back, __('导入完成', 'aksrt'), $message, $result['failed'] > 0);
|
||||
}
|
||||
|
||||
$num = (int) ($_POST['auto_num'] ?? 0);
|
||||
if ($num < 1) {
|
||||
$this->redirect_with_notice($back, __('错误!', 'aksrt'), __('请输入需要生成的数量', 'aksrt'), true);
|
||||
}
|
||||
$code_length = !empty($_POST['auto_top_s'])
|
||||
? max(4, (int) ($_POST['auto_rand_password_limit'] ?? 8))
|
||||
: self::CODE_LENGTH;
|
||||
$codes = $this->generate_batch([
|
||||
'count' => (int) $request->get_param('count'),
|
||||
'max_uses' => (int) $request->get_param('max_uses'),
|
||||
'expires_days' => (int) $request->get_param('expires_days'),
|
||||
'remark' => (string) $request->get_param('remark'),
|
||||
'rewards' => is_array($rewards) ? $rewards : [],
|
||||
'count' => $num,
|
||||
'code_length' => $code_length,
|
||||
'remark' => sanitize_text_field(wp_unslash((string) ($_POST['auto_remarks'] ?? ''))),
|
||||
'rewards' => [
|
||||
'level_points' => (int) ($_POST['level_integral'] ?? 0),
|
||||
'points' => (int) ($_POST['points'] ?? 0),
|
||||
'balance' => (float) ($_POST['balance'] ?? 0),
|
||||
'vip_level' => (int) ($_POST['vip'] ?? 0),
|
||||
'vip_days' => (int) ($_POST['vip_time'] ?? 0),
|
||||
],
|
||||
]);
|
||||
if (is_wp_error($codes)) {
|
||||
return $codes;
|
||||
$this->redirect_with_notice($back, __('错误!', 'aksrt'), $codes->get_error_message(), true);
|
||||
}
|
||||
return new \WP_REST_Response(['success' => true, 'codes' => $codes, 'count' => count($codes)], 201);
|
||||
$this->redirect_with_notice($back, __('完成!', 'aksrt'), sprintf(__('已自动生成%d个邀请码', 'aksrt'), count($codes)));
|
||||
}
|
||||
|
||||
public function rest_export(): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
$table = $this->table();
|
||||
$rows = $wpdb->get_results("SELECT code, batch_id, remark, rewards, max_uses, used_count, status, expires_at, created_at FROM {$table} ORDER BY id DESC LIMIT 10000", ARRAY_A);
|
||||
$lines = ['code,batch_id,remark,rewards,max_uses,used_count,status,expires_at,created_at'];
|
||||
foreach ((array) $rows as $row) {
|
||||
$row['rewards'] = $this->reward_param_string((string) ($row['rewards'] ?? ''));
|
||||
$lines[] = implode(',', array_map(static fn($value) => '"' . str_replace('"', '""', (string) $value) . '"', array_values($row)));
|
||||
/** 列表批量删除。 */
|
||||
public function handle_delete(): void {
|
||||
$back = $this->guard_and_redirect();
|
||||
$ids = array_map('intval', (array) ($_POST['action_id'] ?? []));
|
||||
if ($ids === []) {
|
||||
$this->redirect_with_notice($back, __('错误!', 'aksrt'), __('未选择需要删除的内容', 'aksrt'), true);
|
||||
}
|
||||
$response = new \WP_REST_Response(implode("\n", $lines), 200);
|
||||
$response->set_headers([
|
||||
'Content-Type' => 'text/csv; charset=utf-8',
|
||||
'Content-Disposition' => 'attachment; filename="aksrt-invite-codes.csv"',
|
||||
]);
|
||||
return $response;
|
||||
$deleted = $this->delete_codes($ids);
|
||||
$this->redirect_with_notice($back, __('删除完成', 'aksrt'), sprintf(__('已删除%d个邀请码', 'aksrt'), $deleted));
|
||||
}
|
||||
|
||||
/** 文本导出:与导入格式互通(`码 分隔符 奖励 标识`),导出即可回流导入。 */
|
||||
public function rest_export_text(\WP_REST_Request $request): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
$table = $this->table();
|
||||
$division = trim((string) ($request['division'] ?? ' '));
|
||||
/** 导出视图提交:文本(可回流导入)或 Excel/CSV 表格。 */
|
||||
public function handle_export(): void {
|
||||
if (!current_user_can('edit_users')) {
|
||||
wp_die(esc_html__('您没有权限执行此操作。', 'aksrt'), '', ['response' => 403]);
|
||||
}
|
||||
check_admin_referer('aksrt_invite_admin', 'aksrt_nonce');
|
||||
$status = in_array(($_GET['status'] ?? 'all'), ['all', 'unused', 'used'], true) ? (string) $_GET['status'] : 'all';
|
||||
$format = ($_GET['export_format'] ?? 'xls') === 'text' ? 'text' : 'xls';
|
||||
$division = trim((string) wp_unslash($_GET['text_division'] ?? ' '));
|
||||
if ($division === '') {
|
||||
$division = ' ';
|
||||
}
|
||||
$rows = $wpdb->get_results("SELECT code, remark, rewards FROM {$table} ORDER BY id DESC LIMIT 10000", ARRAY_A);
|
||||
$lines = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$line = $row['code'] . $division . $this->reward_param_string((string) ($row['rewards'] ?? ''));
|
||||
if ((string) ($row['remark'] ?? '') !== '') {
|
||||
$line .= $division . $row['remark'];
|
||||
[$status_sql, $status_params] = $this->status_where($status);
|
||||
|
||||
global $wpdb;
|
||||
$table = $this->table();
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT code, remark, rewards, max_uses, used_count, created_at, updated_at FROM {$table} WHERE {$status_sql} ORDER BY id DESC LIMIT 10000",
|
||||
$status_params
|
||||
), ARRAY_A);
|
||||
|
||||
if ($format === 'text') {
|
||||
$lines = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$line = $row['code'] . $division . $this->reward_param_string((string) ($row['rewards'] ?? ''));
|
||||
if ((string) ($row['remark'] ?? '') !== '') {
|
||||
$line .= $division . $row['remark'];
|
||||
}
|
||||
$lines[] = $line;
|
||||
}
|
||||
$lines[] = $line;
|
||||
$this->stream_download('aksrt-invite-codes.txt', 'text/plain; charset=utf-8', implode("\n", $lines));
|
||||
}
|
||||
$response = new \WP_REST_Response(implode("\n", $lines), 200);
|
||||
$response->set_headers([
|
||||
'Content-Type' => 'text/plain; charset=utf-8',
|
||||
'Content-Disposition' => 'attachment; filename="aksrt-invite-codes.txt"',
|
||||
]);
|
||||
return $response;
|
||||
|
||||
$lines = [implode(',', array_map(static fn($h) => '"' . $h . '"', [__('邀请码', 'aksrt'), __('使用奖励', 'aksrt'), __('创建时间', 'aksrt'), __('更新时间', 'aksrt'), __('状态', 'aksrt'), __('标识', 'aksrt')]))];
|
||||
foreach ((array) $rows as $row) {
|
||||
$used = (int) $row['used_count'];
|
||||
$cells = [
|
||||
(string) $row['code'],
|
||||
$this->reward_param_string((string) ($row['rewards'] ?? '')),
|
||||
(string) $row['created_at'],
|
||||
(string) $row['updated_at'],
|
||||
$used > 0 ? __('已使用', 'aksrt') : __('未使用', 'aksrt'),
|
||||
(string) ($row['remark'] ?? ''),
|
||||
];
|
||||
$lines[] = implode(',', array_map(static fn($value) => '"' . str_replace('"', '""', $value) . '"', $cells));
|
||||
}
|
||||
// UTF-8 BOM:保证 Excel 直接打开不乱码。
|
||||
$this->stream_download('aksrt-invite-codes.csv', 'text/csv; charset=utf-8', "\xEF\xBB\xBF" . implode("\n", $lines));
|
||||
}
|
||||
|
||||
private function stream_download(string $filename, string $content_type, string $body): never {
|
||||
nocache_headers();
|
||||
header('Content-Type: ' . $content_type);
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Content-Length: ' . strlen($body));
|
||||
echo $body;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user