feat(payment): 卡密管理改为独立页面并对齐参考主题
- 移除「余额充值」设置中的充值卡(card_pass_*)配置组,PHP schema 与 SPA mirror 双轨同步 - 重写后台卡密管理页:列表过滤/排序/搜索/批量删除/同款分页/点击复制、自动生成与导入、text/xls 导出 - ChargeCard 服务新增 generate_cards/rand_number/rand_password/delete_by_ids/get_export_rows,get_list 支持参考主题排序别名与标识过滤 - 删除无消费方的 /payment/charge-cards REST 路由及 batch_create/disable 死代码,数据层零迁移
This commit is contained in:
+1066
-296
File diff suppressed because it is too large
Load Diff
@@ -41,74 +41,6 @@ class PaymentController extends ControllerBase {
|
||||
'note' => ['type' => 'string', 'default' => '', 'maxLength' => 1000, 'sanitize_callback' => 'sanitize_textarea_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// ── Charge Card ──
|
||||
|
||||
// Charge card list
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/charge-cards', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_charge_cards'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1],
|
||||
'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 20],
|
||||
'status' => ['type' => 'string', 'enum' => ['', 'active', 'used', 'expired', 'disabled']],
|
||||
'keyword' => ['type' => 'string', 'maxLength' => 191, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'date_from' => ['type' => 'string', 'format' => 'date'],
|
||||
'date_to' => ['type' => 'string', 'format' => 'date'],
|
||||
],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_charge_card_detail'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => ['id' => ['type' => 'integer', 'minimum' => 1, 'required' => true]],
|
||||
]);
|
||||
|
||||
// Charge card create
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/charge-cards', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_charge_card'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'type' => ['type' => 'string', 'required' => true, 'enum' => ['balance', 'vip', 'credits']],
|
||||
'password' => ['type' => 'string', 'required' => true, 'minLength' => 1, 'maxLength' => 80, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'amount' => ['type' => 'string', 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
|
||||
'points' => ['type' => 'integer', 'minimum' => 1],
|
||||
'level' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 2],
|
||||
'days' => ['type' => 'integer', 'minimum' => 0],
|
||||
'expires_at' => ['type' => 'string', 'maxLength' => 32, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'note' => ['type' => 'string', 'maxLength' => 1000, 'sanitize_callback' => 'sanitize_textarea_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Charge card batch create
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/batch', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'batch_create_charge_cards'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'type' => ['type' => 'string', 'required' => true, 'enum' => ['balance', 'vip', 'credits']],
|
||||
'count' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'required' => true],
|
||||
'amount' => ['type' => 'string', 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
|
||||
'points' => ['type' => 'integer', 'minimum' => 1],
|
||||
'level' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 2],
|
||||
'days' => ['type' => 'integer', 'minimum' => 0],
|
||||
'prefix' => ['type' => 'string', 'maxLength' => 20, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'length' => ['type' => 'integer', 'minimum' => 8, 'maximum' => 32, 'default' => 16],
|
||||
'expires_at' => ['type' => 'string', 'maxLength' => 32, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'note' => ['type' => 'string', 'maxLength' => 1000, 'sanitize_callback' => 'sanitize_textarea_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Charge card disable
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/(?P<id>\d+)/disable', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'disable_charge_card'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => ['id' => ['type' => 'integer', 'minimum' => 1, 'required' => true]],
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
public function get_withdrawals(\WP_REST_Request $request): \WP_REST_Response {
|
||||
@@ -178,287 +110,4 @@ class PaymentController extends ControllerBase {
|
||||
return $this->error(__('更新失败', 'aksrt'));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Charge Card
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* GET /payment/charge-cards — 充值卡列表
|
||||
*/
|
||||
public function get_charge_cards(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$table = \AKSRT\Core\DBMigrator::table('charge_card');
|
||||
$page = max(1, (int) ($request->get_param('page') ?: 1));
|
||||
$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
$keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
|
||||
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
|
||||
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
if ($date_from !== '') {
|
||||
$where[] = 'created_at >= %s';
|
||||
$params[] = $date_from . ' 00:00:00';
|
||||
}
|
||||
if ($date_to !== '') {
|
||||
$where[] = 'created_at <= %s';
|
||||
$params[] = $date_to . ' 23:59:59';
|
||||
}
|
||||
|
||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
$sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT 500";
|
||||
$rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
$items = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$item = $this->build_charge_card_admin_item($row);
|
||||
if ($keyword !== '' && mb_stripos(implode(' ', array_filter([
|
||||
(string) ($item['password'] ?? ''),
|
||||
(string) ($item['card'] ?? ''),
|
||||
(string) ($item['type_label'] ?? ''),
|
||||
(string) ($item['note'] ?? ''),
|
||||
(string) ($item['used_by_user']['display_name'] ?? ''),
|
||||
(string) ($item['used_by_user']['email'] ?? ''),
|
||||
])), $keyword) === false) {
|
||||
continue;
|
||||
}
|
||||
$items[] = $item;
|
||||
}
|
||||
|
||||
$total = count($items);
|
||||
$offset = ($page - 1) * $per_page;
|
||||
$stats = \AKSRT\Payment\ChargeCard::get_instance()->get_stats();
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => array_values(array_slice($items, $offset, $per_page)),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'max_pages' => max(1, (int) ceil(max(1, $total) / $per_page)),
|
||||
'stats' => $stats,
|
||||
'options' => [
|
||||
'statuses' => [
|
||||
['label' => __('全部状态', 'aksrt'), 'value' => ''],
|
||||
['label' => __('未使用', 'aksrt'), 'value' => 'active'],
|
||||
['label' => __('已使用', 'aksrt'), 'value' => 'used'],
|
||||
['label' => __('已过期', 'aksrt'), 'value' => 'expired'],
|
||||
['label' => __('已禁用', 'aksrt'), 'value' => 'disabled'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/charge-cards — 创建充值卡
|
||||
*/
|
||||
public function create_charge_card(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
|
||||
return $this->error('充值卡系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$type = sanitize_key((string) ($params['type'] ?? 'balance'));
|
||||
if (!in_array($type, ['balance', 'vip', 'credits'], true)) {
|
||||
$type = 'balance';
|
||||
}
|
||||
$password = sanitize_text_field((string) ($params['password'] ?? ''));
|
||||
if ($password === '') {
|
||||
return $this->error('卡密不能为空');
|
||||
}
|
||||
|
||||
$id = \AKSRT\Payment\ChargeCard::get_instance()->create(array_merge($params, [
|
||||
'type' => $type,
|
||||
'password' => $password,
|
||||
]));
|
||||
|
||||
if ($id > 0) {
|
||||
return $this->success([
|
||||
'message' => __('充值卡已创建', 'aksrt'),
|
||||
'data' => ['id' => $id],
|
||||
], 201);
|
||||
}
|
||||
|
||||
return $this->error(__('创建失败(可能卡密已存在或参数无效)', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/charge-cards/batch — 批量生成充值卡
|
||||
*/
|
||||
public function batch_create_charge_cards(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
|
||||
return $this->error('充值卡系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$type = sanitize_key((string) ($params['type'] ?? 'balance'));
|
||||
if (!in_array($type, ['balance', 'vip', 'credits'], true)) {
|
||||
$type = 'balance';
|
||||
}
|
||||
$count = max(1, min(100, (int) ($params['count'] ?? 1)));
|
||||
$length = max(8, min(32, (int) ($params['length'] ?? 16)));
|
||||
$prefix = sanitize_text_field((string) ($params['prefix'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
$expires = sanitize_text_field((string) ($params['expires_at'] ?? ''));
|
||||
|
||||
$raw_amount = $params['amount'] ?? '';
|
||||
$meta_fields = [
|
||||
'amount' => is_string($raw_amount) ? $raw_amount : '',
|
||||
'level' => (int) ($params['level'] ?? 1),
|
||||
'days' => (int) ($params['days'] ?? 30),
|
||||
'points' => (int) ($params['points'] ?? 0),
|
||||
];
|
||||
|
||||
if ($type === 'balance') {
|
||||
try {
|
||||
$card_amount = \AKSRT\Ledger\AssetAmount::balance($meta_fields['amount']);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->error('余额卡金额格式无效');
|
||||
}
|
||||
if ($card_amount->minor() <= 0) return $this->error('余额卡金额必须大于 0');
|
||||
$meta_fields['amount'] = $card_amount->decimal();
|
||||
}
|
||||
if ($type === 'credits' && $meta_fields['points'] <= 0) {
|
||||
return $this->error('积分卡积分必须大于 0');
|
||||
}
|
||||
|
||||
$codes = \AKSRT\Payment\ChargeCard::get_instance()->batch_create(
|
||||
$type,
|
||||
$meta_fields,
|
||||
$count,
|
||||
$length,
|
||||
$prefix,
|
||||
$expires,
|
||||
$note
|
||||
);
|
||||
|
||||
return $this->success([
|
||||
'message' => sprintf(__('已生成 %d 张充值卡', 'aksrt'), count($codes)),
|
||||
'data' => ['codes' => $codes, 'count' => count($codes), 'type' => $type],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/charge-cards/{id}/disable — 禁用充值卡
|
||||
*/
|
||||
public function disable_charge_card(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
|
||||
return $this->error('充值卡系统未启用', 500);
|
||||
}
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
if ($id <= 0) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$success = \AKSRT\Payment\ChargeCard::get_instance()->disable($id);
|
||||
|
||||
if ($success) {
|
||||
return $this->success(['message' => __('充值卡已禁用', 'aksrt')]);
|
||||
}
|
||||
return $this->error(__('操作失败', 'aksrt'));
|
||||
}
|
||||
|
||||
public function get_charge_card_detail(\WP_REST_Request $request): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
$table = \AKSRT\Core\DBMigrator::table('charge_card');
|
||||
|
||||
if ($id <= 0 || !$table) {
|
||||
return $this->error(__('充值卡不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A);
|
||||
if (!$row) {
|
||||
return $this->error(__('充值卡不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'card' => $this->build_charge_card_admin_item($row),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function build_charge_card_admin_item(array $row): array {
|
||||
$used_by = (int) ($row['used_by'] ?? 0);
|
||||
$user = $used_by > 0 ? get_userdata($used_by) : null;
|
||||
$status = sanitize_key((string) ($row['status'] ?? 'active'));
|
||||
$type = sanitize_key((string) ($row['type'] ?? 'balance'));
|
||||
$statusMap = [
|
||||
'active' => __('未使用', 'aksrt'),
|
||||
'used' => __('已使用', 'aksrt'),
|
||||
'expired' => __('已过期', 'aksrt'),
|
||||
'disabled' => __('已禁用', 'aksrt'),
|
||||
];
|
||||
$typeMap = [
|
||||
'balance' => __('余额卡', 'aksrt'),
|
||||
'vip' => __('会员卡', 'aksrt'),
|
||||
'credits' => __('积分卡', 'aksrt'),
|
||||
];
|
||||
|
||||
$meta = [];
|
||||
if (!empty($row['meta'])) {
|
||||
if (is_array($row['meta'])) {
|
||||
$meta = $row['meta'];
|
||||
} else {
|
||||
$decoded = json_decode((string) $row['meta'], true);
|
||||
if (is_array($decoded)) {
|
||||
$meta = $decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$password = (string) ($row['password'] ?? '');
|
||||
$card_no = (string) ($row['card'] ?? '');
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'password' => $password,
|
||||
'card' => $card_no,
|
||||
'type' => $type,
|
||||
'type_label' => $typeMap[$type] ?? $type,
|
||||
'amount' => $this->cash_decimal((string) ($meta['amount'] ?? $row['amount'] ?? '0')),
|
||||
'balance' => $this->cash_decimal((string) ($row['balance'] ?? '0')),
|
||||
'meta' => $meta,
|
||||
'level' => (int) ($meta['level'] ?? 0),
|
||||
'days' => (int) ($meta['days'] ?? 0),
|
||||
'points' => (int) ($meta['points'] ?? 0),
|
||||
'order_number' => (string) ($row['order_number'] ?? ''),
|
||||
'status' => $status,
|
||||
'status_label' => $statusMap[$status] ?? $status,
|
||||
'used_by' => $used_by,
|
||||
'used_at' => (string) ($row['used_at'] ?? ''),
|
||||
'expires_at' => (string) ($row['expires_at'] ?? ''),
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'note' => (string) ($row['note'] ?? ''),
|
||||
'used_by_user' => $user instanceof \WP_User ? [
|
||||
'id' => (int) $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'email' => $user->user_email,
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function cash_decimal(string $value): string {
|
||||
try {
|
||||
return \AKSRT\Ledger\AssetAmount::balance($value)->decimal();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return '0.00';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ class SchemaPayment {
|
||||
'recharge' => [
|
||||
'key' => 'recharge',
|
||||
'title' => '余额充值',
|
||||
'version' => '1.1.0',
|
||||
'version' => '1.2.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'notice', 'title' => '说明', 'fields' => [
|
||||
@@ -159,26 +159,6 @@ class SchemaPayment {
|
||||
['key' => 'pay_balance_pass_charge_desc', 'label' => '卡密充值说明', 'type' => 'textarea', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true], 'help' => '用户使用卡密充值时显示的内容,建议为提醒事项、充值协议等,支持HTML代码'],
|
||||
['key' => 'pay_balance_pass_charge_only_password', 'label' => '单密码模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true], 'help' => '仅需输入密码,不需要卡号'],
|
||||
]],
|
||||
['key' => 'charge-card', 'title' => '充值卡', 'fields' => [
|
||||
['key' => 'card_pass_enabled', 'label' => '启用充值卡', 'type' => 'switch', 'default' => false, 'help' => '充值卡规则与批量发卡配置;卡密兑换入口由上方「卡密充值」控制'],
|
||||
['key' => 'card_pass_desc', 'label' => '充值卡说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_type', 'label' => '充值卡类型', 'type' => 'select', 'default' => 'balance', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true], 'options' => [
|
||||
['label' => '余额卡', 'value' => 'balance'],
|
||||
['label' => 'VIP卡', 'value' => 'vip'],
|
||||
['label' => '积分卡', 'value' => 'credits'],
|
||||
]],
|
||||
['key' => 'card_pass_prefix', 'label' => '卡号前缀', 'type' => 'input', 'default' => 'AK', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_length', 'label' => '卡号长度', 'type' => 'number', 'default' => 16, 'min' => 8, 'max' => 32, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_batch_size', 'label' => '批量生成数量', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_max_redeem', 'label' => '单卡最大兑换次数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_expiry', 'label' => '有效期(天)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为永久有效', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_amount', 'label' => '面额(元)', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '10.00', 'min' => 1, 'max' => 9999, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_vip_days', 'label' => 'VIP天数', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 3650, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_credits', 'label' => '赠送积分', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_redeem_page', 'label' => '兑换页面', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_success_msg', 'label' => '兑换成功提示', 'type' => 'input', 'default' => '兑换成功!', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_fail_msg', 'label' => '兑换失败提示', 'type' => 'input', 'default' => '卡号无效或已过期', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'points-exchange', 'title' => '余额兑换积分', 'fields' => [
|
||||
['key' => 'pay_balance_points_exchange_s', 'label' => '启用余额兑换积分', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
['key' => 'pay_balance_points_exchange_rate', 'label' => '兑换比例', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000, 'help' => '1 余额 = N 积分', 'dependsOn' => ['key' => 'pay_balance_points_exchange_s', 'value' => true]],
|
||||
|
||||
+156
-27
@@ -14,6 +14,12 @@ class ChargeCard {
|
||||
public const TYPE_BALANCE = 'balance';
|
||||
public const TYPE_VIP = 'vip';
|
||||
public const TYPE_CREDITS = 'credits';
|
||||
/** 自定义卡密:仅用于后台生成/导入/导出管理,不参与前台兑换流程。 */
|
||||
public const TYPE_CUSTOM = 'custom';
|
||||
|
||||
public const TYPE_KEYS = [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS, self::TYPE_CUSTOM];
|
||||
/** 可被前台兑换的类型(custom 仅后台管理)。 */
|
||||
public const REDEEMABLE_TYPES = [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS];
|
||||
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
public const STATUS_USED = 'used';
|
||||
@@ -208,7 +214,7 @@ class ChargeCard {
|
||||
global $wpdb;
|
||||
|
||||
$type = sanitize_key((string) ($data['type'] ?? self::TYPE_BALANCE));
|
||||
if (!in_array($type, [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS], true)) {
|
||||
if (!in_array($type, self::TYPE_KEYS, true)) {
|
||||
$type = self::TYPE_BALANCE;
|
||||
}
|
||||
|
||||
@@ -256,32 +262,63 @@ class ChargeCard {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $meta_fields
|
||||
* @return array<int,string> passwords/codes generated
|
||||
* 批量自动生成卡密(参考主题 zibpay_generate_pass_card 同款行为)。
|
||||
*
|
||||
* 每张卡同时生成随机卡号与随机密码;$meta 由 normalize_meta 校验,
|
||||
* 与「卡密管理」页面的自动生成表单一一对应。
|
||||
*
|
||||
* @return int 成功生成的张数
|
||||
*/
|
||||
public function batch_create(string $type, array $meta_fields, int $count, int $length = 16, string $prefix = '', string $expires_at = '', string $note = ''): array {
|
||||
public function generate_cards(string $type, int $num, array $meta, int $rand_number = 20, int $rand_password = 35, string $note = ''): int {
|
||||
$type = sanitize_key($type);
|
||||
if (!in_array($type, [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS], true)) {
|
||||
$type = self::TYPE_BALANCE;
|
||||
if (!in_array($type, self::TYPE_KEYS, true)) {
|
||||
return 0;
|
||||
}
|
||||
$count = max(1, min(500, $count));
|
||||
$length = max(8, min(32, $length));
|
||||
$prefix = sanitize_text_field($prefix);
|
||||
$codes = [];
|
||||
$num = max(1, min(1000, $num));
|
||||
$rand_number = max(0, min(50, $rand_number));
|
||||
$rand_password = max(1, min(50, $rand_password));
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$password = $prefix . strtoupper(wp_generate_password($length, false));
|
||||
$id = $this->create(array_merge($meta_fields, [
|
||||
'type' => $type,
|
||||
'password' => $password,
|
||||
'expires_at' => $expires_at,
|
||||
'note' => $note,
|
||||
]));
|
||||
$created = 0;
|
||||
for ($i = 1; $i <= $num; $i++) {
|
||||
$id = $this->create([
|
||||
'card' => self::rand_number($rand_number),
|
||||
'password' => self::rand_password($rand_password),
|
||||
'type' => $type,
|
||||
'meta' => $meta,
|
||||
'note' => $note,
|
||||
]);
|
||||
if ($id > 0) {
|
||||
$codes[] = $password;
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
return $codes;
|
||||
return $created;
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机卡号(纯数字,参考主题同款算法)。
|
||||
*/
|
||||
public static function rand_number(int $limit = 20): string {
|
||||
if ($limit <= 0) {
|
||||
return '';
|
||||
}
|
||||
$chars = (string) mt_rand(100, 999) . (string) mt_rand(100, 999) . (string) mt_rand(100, 999);
|
||||
if ($limit > 27) {
|
||||
$chars = $chars . $chars;
|
||||
}
|
||||
$chars = str_shuffle($chars . $chars . $chars);
|
||||
return substr($chars, 0, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机密码(字母+数字混合,参考主题同款算法)。
|
||||
*/
|
||||
public static function rand_password(int $limit = 35): string {
|
||||
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890';
|
||||
if ($limit > 60) {
|
||||
$chars = $chars . $chars;
|
||||
}
|
||||
$chars = str_shuffle($chars);
|
||||
return substr($chars, 0, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -794,7 +831,16 @@ class ChargeCard {
|
||||
if ($type === self::TYPE_VIP) {
|
||||
$level = max(1, min(2, (int) ($raw['level'] ?? $data['level'] ?? 1)));
|
||||
$days = max(0, (int) ($raw['days'] ?? $data['days'] ?? 0));
|
||||
return ['level' => $level, 'days' => $days];
|
||||
$meta = ['level' => $level, 'days' => $days];
|
||||
// 卡密管理页按参考主题保存 time/unit(Permanent|天数 + day|month),
|
||||
// 仅作展示与导出使用;兑换仍以 days 为准。
|
||||
if (isset($raw['time'])) {
|
||||
$meta['time'] = sanitize_text_field((string) $raw['time']);
|
||||
}
|
||||
if (isset($raw['unit'])) {
|
||||
$meta['unit'] = sanitize_key((string) $raw['unit']) === 'month' ? 'month' : 'day';
|
||||
}
|
||||
return $meta;
|
||||
}
|
||||
if ($type === self::TYPE_CREDITS) {
|
||||
$points = max(0, (int) ($raw['points'] ?? $data['points'] ?? 0));
|
||||
@@ -803,6 +849,21 @@ class ChargeCard {
|
||||
}
|
||||
return ['points' => $points];
|
||||
}
|
||||
if ($type === self::TYPE_CUSTOM) {
|
||||
$meta = [];
|
||||
$custom_name = $raw['custom_name'] ?? $data['custom_name'] ?? [];
|
||||
if (is_array($custom_name)) {
|
||||
$card_name = trim(sanitize_text_field((string) ($custom_name['card_name'] ?? '')));
|
||||
$password_name = trim(sanitize_text_field((string) ($custom_name['password_name'] ?? '')));
|
||||
if ($card_name !== '') {
|
||||
$meta['custom_name']['card_name'] = $card_name;
|
||||
}
|
||||
if ($password_name !== '') {
|
||||
$meta['custom_name']['password_name'] = $password_name;
|
||||
}
|
||||
}
|
||||
return $meta;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -828,6 +889,9 @@ class ChargeCard {
|
||||
/**
|
||||
* Admin list helper.
|
||||
*
|
||||
* Supported args: limit, offset, status, type, search/s, other(标识),
|
||||
* orderby(白名单,参考主题别名自动映射), order(ASC|DESC).
|
||||
*
|
||||
* @return array{items:array,total:int,page:int,max_pages:int}
|
||||
*/
|
||||
public function get_list(array $args = []): array {
|
||||
@@ -838,6 +902,7 @@ class ChargeCard {
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
$type = sanitize_key((string) ($args['type'] ?? ''));
|
||||
$search = sanitize_text_field((string) ($args['search'] ?? $args['s'] ?? ''));
|
||||
$other = sanitize_text_field((string) ($args['other'] ?? ''));
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
@@ -849,9 +914,14 @@ class ChargeCard {
|
||||
$where[] = 'type = %s';
|
||||
$params[] = $type;
|
||||
}
|
||||
if ($other !== '') {
|
||||
$where[] = 'note = %s';
|
||||
$params[] = $other;
|
||||
}
|
||||
if ($search !== '') {
|
||||
$like = '%' . $wpdb->esc_like($search) . '%';
|
||||
$where[] = '(password LIKE %s OR card LIKE %s OR note LIKE %s OR CAST(id AS CHAR) = %s)';
|
||||
$where[] = '(password LIKE %s OR card LIKE %s OR note LIKE %s OR meta LIKE %s OR CAST(id AS CHAR) = %s)';
|
||||
$params[] = $like;
|
||||
$params[] = $like;
|
||||
$params[] = $like;
|
||||
$params[] = $like;
|
||||
@@ -864,7 +934,10 @@ class ChargeCard {
|
||||
? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params))
|
||||
: (int) $wpdb->get_var($count_sql);
|
||||
|
||||
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||
$orderby = $this->resolve_orderby((string) ($args['orderby'] ?? ''));
|
||||
$order = strtoupper((string) ($args['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC';
|
||||
|
||||
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
$items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
|
||||
@@ -877,11 +950,67 @@ class ChargeCard {
|
||||
];
|
||||
}
|
||||
|
||||
public function disable(int $id): bool {
|
||||
/**
|
||||
* 排序字段白名单;兼容参考主题的列名别名。
|
||||
* modified_time 以 used_at 兜底(未使用卡等于创建时间)。
|
||||
*/
|
||||
private function resolve_orderby(string $orderby): string {
|
||||
$map = [
|
||||
'id' => 'id',
|
||||
'card' => 'card',
|
||||
'password' => 'password',
|
||||
'type' => 'type',
|
||||
'status' => 'status',
|
||||
'note' => 'note',
|
||||
'other' => 'note',
|
||||
'create_time' => 'created_at',
|
||||
'created_at' => 'created_at',
|
||||
'modified_time' => 'COALESCE(used_at, created_at)',
|
||||
'modified' => 'COALESCE(used_at, created_at)',
|
||||
];
|
||||
$key = sanitize_key(trim($orderby));
|
||||
return $map[$key] ?? 'COALESCE(used_at, created_at)';
|
||||
}
|
||||
|
||||
/**
|
||||
* 按主键批量删除。
|
||||
*
|
||||
* @param array<int,int> $ids
|
||||
*/
|
||||
public function delete_by_ids(array $ids): int {
|
||||
global $wpdb;
|
||||
return (bool) $wpdb->update($this->table(), [
|
||||
'status' => self::STATUS_DISABLED,
|
||||
], ['id' => $id], ['%s'], ['%d']);
|
||||
$ids = array_values(array_unique(array_filter(array_map('absint', $ids))));
|
||||
if (!$ids) {
|
||||
return 0;
|
||||
}
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
|
||||
return (int) $wpdb->query($wpdb->prepare("DELETE FROM {$this->table()} WHERE id IN ({$placeholders})", $ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出用全量行(不限分页)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
public function get_export_rows(string $type = '', string $status = ''): array {
|
||||
global $wpdb;
|
||||
$type = sanitize_key($type);
|
||||
$status = sanitize_key($status);
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
if ($type !== '' && in_array($type, self::TYPE_KEYS, true)) {
|
||||
$where[] = 'type = %s';
|
||||
$params[] = $type;
|
||||
}
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
$sql = "SELECT card, password, type, meta, note, status FROM {$this->table()} {$where_sql} ORDER BY id ASC";
|
||||
$rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A);
|
||||
return is_array($rows) ? $rows : [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -175,15 +175,6 @@ trait CreditsPaymentGroups {
|
||||
return PaymentRuntime::get_order_settings();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取充值卡设置
|
||||
*
|
||||
* @return array{enabled:bool,desc:string,type:string,prefix:string,length:int,batch_size:int,max_redeem:int,expiry_days:int,amount:string,vip_days:int,credits:int,redeem_page:string,success_msg:string,fail_msg:string}
|
||||
*/
|
||||
public static function get_card_pass_settings(): array {
|
||||
return PaymentRuntime::get_card_pass_settings();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提现管理设置
|
||||
*
|
||||
|
||||
@@ -272,7 +272,6 @@ trait DisplayPageGroups {
|
||||
'affiliate' => self::get_payment_affiliate_settings(),
|
||||
'gateway' => self::get_payment_gateway_settings(),
|
||||
'order' => self::get_order_settings(),
|
||||
'card_pass' => self::get_card_pass_settings(),
|
||||
'withdrawal' => self::get_withdrawal_settings(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -242,10 +242,6 @@ class HomeCms {
|
||||
return PaymentRuntime::get_order_settings();
|
||||
}
|
||||
|
||||
public static function get_card_pass_settings(): array {
|
||||
return PaymentRuntime::get_card_pass_settings();
|
||||
}
|
||||
|
||||
public static function get_withdrawal_settings(): array {
|
||||
return PaymentRuntime::get_withdrawal_settings();
|
||||
}
|
||||
|
||||
@@ -504,38 +504,8 @@ class PaymentRuntime {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @return array{min_amount:string,service_charge_rate:int,daily_count:int,daily_limit:string,reset_time:string,payout_channels:string[]}
|
||||
*/
|
||||
public static function get_card_pass_settings(): array {
|
||||
$s = Settings::get_all();
|
||||
|
||||
$type = sanitize_key((string) ($s['card_pass_type'] ?? 'balance'));
|
||||
if (!in_array($type, ['balance', 'vip', 'credits'], true)) {
|
||||
$type = 'balance';
|
||||
}
|
||||
|
||||
$success_msg = trim((string) ($s['card_pass_success_msg'] ?? ''));
|
||||
$fail_msg = trim((string) ($s['card_pass_fail_msg'] ?? ''));
|
||||
|
||||
return [
|
||||
'enabled' => !empty($s['card_pass_enabled']),
|
||||
'desc' => sanitize_textarea_field((string) ($s['card_pass_desc'] ?? '')),
|
||||
'type' => $type,
|
||||
'prefix' => sanitize_text_field((string) ($s['card_pass_prefix'] ?? 'AK')),
|
||||
'length' => max(8, min(32, (int) ($s['card_pass_length'] ?? 16))),
|
||||
'batch_size' => max(1, min(500, (int) ($s['card_pass_batch_size'] ?? 50))),
|
||||
'max_redeem' => max(1, min(100, (int) ($s['card_pass_max_redeem'] ?? 1))),
|
||||
'expiry_days' => max(0, (int) ($s['card_pass_expiry'] ?? 0)),
|
||||
'amount' => self::money_decimal($s['card_pass_amount'] ?? '10.00', '10.00', 999900),
|
||||
'vip_days' => max(1, min(3650, (int) ($s['card_pass_vip_days'] ?? 30))),
|
||||
'credits' => max(0, min(99999, (int) ($s['card_pass_credits'] ?? 0))),
|
||||
'redeem_page' => esc_url_raw((string) ($s['card_pass_redeem_page'] ?? '')),
|
||||
'success_msg' => $success_msg !== '' ? $success_msg : __('兑换成功!', 'aksrt'),
|
||||
'fail_msg' => $fail_msg !== '' ? $fail_msg : __('卡号无效或已过期', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{min_amount:string,service_charge_rate:int,daily_count:int,daily_limit:string,reset_time:string,payout_channels:string[]} */
|
||||
public static function get_withdrawal_settings(): array {
|
||||
$s = Settings::get_all();
|
||||
$channels = array_values(array_intersect(['alipay', 'wechat'], array_map('sanitize_key', (array) ($s['affiliate_payout_channels'] ?? ['alipay', 'wechat']))));
|
||||
|
||||
Reference in New Issue
Block a user