Files
wordpress-theme-aksrt/inc/classes/payment/class-aksrt-charge-card.php
T
kate522 ce1c13b947 refactor(classes): organize inc/classes into domain folders
Move ~94 root class shells and root *-traits into domain directories
(core, auth, user, credits, content, payment/gateways, forum, shop,
sms, ai, messages, extensions, widgets, settings, admin). Update
bootstrap require paths, co-locate traits with owners, fix forum/user
relative requires, drop duplicate shop controller requires, and wire
home-modules load. Path-only; class names and init order unchanged.
2026-07-17 11:11:57 +08:00

728 lines
28 KiB
PHP

<?php
/**
* AKSRT Charge Card — 卡密(余额 / VIP / 积分)
*
* zibll-style card_pass: typed inventory, single-use CAS redeem,
* public REST for user-center, admin create/batch.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Charge_Card {
private static $instance = null;
public const TYPE_BALANCE = 'balance';
public const TYPE_VIP = 'vip';
public const TYPE_CREDITS = 'credits';
public const STATUS_ACTIVE = 'active';
public const STATUS_USED = 'used';
public const STATUS_EXPIRED = 'expired';
public const STATUS_DISABLED = 'disabled';
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('rest_api_init', [$this, 'register_public_routes']);
}
private function table(): string {
return AKSRT_DB_Migrator::table('charge_card');
}
public function register_public_routes(): void {
register_rest_route('aksrt/v1', '/pay/card-pass/options', [
'methods' => 'GET',
'callback' => [$this, 'rest_options'],
'permission_callback' => [$this, 'check_rest_nonce'],
]);
register_rest_route('aksrt/v1', '/pay/card-pass/redeem', [
'methods' => 'POST',
'callback' => [$this, 'rest_redeem'],
'permission_callback' => [$this, 'check_logged_in_rest'],
]);
}
/**
* @param WP_REST_Request $request
* @return true|WP_Error
*/
public function check_rest_nonce(WP_REST_Request $request) {
$nonce = $request->get_header('x-wp-nonce');
if (!$nonce) {
return new WP_Error('aksrt_rest_no_nonce', __('缺少安全令牌', 'aksrt'), ['status' => 403]);
}
if (!wp_verify_nonce($nonce, 'wp_rest')) {
return new WP_Error('aksrt_rest_bad_nonce', __('安全令牌已过期,请刷新页面', 'aksrt'), ['status' => 403]);
}
return true;
}
/**
* @param WP_REST_Request $request
* @return true|WP_Error
*/
public function check_logged_in_rest(WP_REST_Request $request) {
$nonce = $this->check_rest_nonce($request);
if (is_wp_error($nonce)) {
return $nonce;
}
if (!is_user_logged_in()) {
return new WP_Error('aksrt_need_login', __('请先登录', 'aksrt'), ['status' => 401]);
}
return true;
}
public function rest_options(WP_REST_Request $request) {
return new WP_REST_Response([
'success' => true,
'data' => $this->get_public_options(),
], 200);
}
public function rest_redeem(WP_REST_Request $request) {
$params = $request->get_json_params();
if (!is_array($params)) {
$params = $request->get_params();
}
$password = sanitize_text_field((string) ($params['password'] ?? $params['code'] ?? ''));
$card = sanitize_text_field((string) ($params['card'] ?? ''));
$type = sanitize_key((string) ($params['type'] ?? ''));
if ($type !== '' && !in_array($type, [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS], true)) {
$type = '';
}
$result = $this->redeem_for_user(get_current_user_id(), $password, $card, $type);
if (is_wp_error($result)) {
return $result;
}
return new WP_REST_Response([
'success' => true,
'data' => $result,
], 200);
}
/**
* @return array<string,mixed>
*/
public function get_public_options(): array {
$balance = ['enabled' => false, 'desc' => '', 'only_password' => true];
$vip = ['enabled' => false, 'desc' => '', 'only_password' => true];
if (class_exists('AKSRT_Settings')) {
if (method_exists('AKSRT_Settings', 'get_payment_recharge_settings')) {
$recharge = AKSRT_Settings::get_payment_recharge_settings();
$pc = is_array($recharge['pass_charge'] ?? null) ? $recharge['pass_charge'] : [];
$balance = [
'enabled' => !empty($pc['enabled']),
'desc' => (string) ($pc['desc'] ?? ''),
'only_password' => !isset($pc['only_password']) || !empty($pc['only_password']),
];
}
if (method_exists('AKSRT_Settings', 'get_payment_vip_settings')) {
$vip_s = AKSRT_Settings::get_payment_vip_settings();
$benefit = is_array($vip_s['benefit'] ?? null) ? $vip_s['benefit'] : [];
$vip = [
'enabled' => !empty($benefit['pass_charge']),
'desc' => (string) ($benefit['pass_charge_desc'] ?? ''),
'only_password' => !isset($benefit['pass_charge_only_password']) || !empty($benefit['pass_charge_only_password']),
];
}
}
// Points card-pass: zibll points_pass_exchange_* (stored in aksrt_settings).
$credits = ['enabled' => false, 'desc' => '', 'only_password' => true];
$all = class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_all')
? AKSRT_Settings::get_all()
: (array) get_option('aksrt_settings', []);
if (!empty($all['points_pass_exchange_s'])) {
// Require credits system on when available.
$credits_on = !class_exists('AKSRT_Settings') || !method_exists('AKSRT_Settings', 'credits_enabled') || AKSRT_Settings::credits_enabled();
$credits = [
'enabled' => $credits_on,
'desc' => sanitize_textarea_field((string) ($all['points_pass_exchange_desc'] ?? '')),
'only_password' => !isset($all['points_pass_exchange_only_password']) || !empty($all['points_pass_exchange_only_password']),
];
}
return [
'balance' => $balance,
'vip' => $vip,
'credits' => $credits,
'labels' => [
'title' => __('卡密兑换', 'aksrt'),
'password' => __('卡密', 'aksrt'),
'card' => __('卡号', 'aksrt'),
'submit_balance' => __('兑换余额', 'aksrt'),
'submit_vip' => __('兑换会员', 'aksrt'),
'submit_credits' => __('兑换积分', 'aksrt'),
'placeholder_pw' => __('请输入卡密', 'aksrt'),
'placeholder_card' => __('请输入卡号', 'aksrt'),
],
];
}
/**
* Create one card.
*
* Accepted $data keys:
* - password|code (required unless auto)
* - card (optional)
* - type balance|vip|credits
* - amount (balance)
* - level, days (vip; days 0 = permanent)
* - points (credits)
* - meta (array override)
* - expires_at, note
*/
public function create(array $data): int {
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)) {
$type = self::TYPE_BALANCE;
}
$password = sanitize_text_field((string) ($data['password'] ?? $data['code'] ?? ''));
if ($password === '') {
return 0;
}
$card = sanitize_text_field((string) ($data['card'] ?? ''));
$meta = $this->normalize_meta($type, $data);
if ($meta === null) {
return 0;
}
// Uniqueness: password alone (only_password mode common).
$exists = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$this->table()} WHERE password = %s LIMIT 1",
$password
));
if ($exists) {
return 0;
}
$expires_at = sanitize_text_field((string) ($data['expires_at'] ?? ''));
$note = sanitize_textarea_field((string) ($data['note'] ?? ''));
$amount_col = (float) ($meta['amount'] ?? $meta['points'] ?? 0);
$result = $wpdb->insert($this->table(), [
'card' => $card,
'password' => $password,
'type' => $type,
'amount' => $amount_col,
'balance' => $amount_col,
'status' => self::STATUS_ACTIVE,
'meta' => wp_json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'order_number' => '',
'used_by' => null,
'used_at' => null,
'expires_at' => $expires_at !== '' ? $expires_at : null,
'created_at' => current_time('mysql'),
'note' => $note,
]);
return $result ? (int) $wpdb->insert_id : 0;
}
/**
* @param array<string,mixed> $meta_fields
* @return array<int,string> passwords/codes generated
*/
public function batch_create(string $type, array $meta_fields, int $count, int $length = 16, string $prefix = '', string $expires_at = '', string $note = ''): array {
$type = sanitize_key($type);
if (!in_array($type, [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS], true)) {
$type = self::TYPE_BALANCE;
}
$count = max(1, min(500, $count));
$length = max(8, min(32, $length));
$prefix = sanitize_text_field($prefix);
$codes = [];
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,
]));
if ($id > 0) {
$codes[] = $password;
}
}
return $codes;
}
/**
* @return array{success:bool,message:string,type?:string,amount?:float,level?:int,days?:int,order_number?:string,balance?:int}|WP_Error
*/
public function redeem_for_user(int $user_id, string $password, string $card = '', string $expected_type = '') {
$user_id = max(0, $user_id);
$password = sanitize_text_field($password);
$card = sanitize_text_field($card);
if ($user_id <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录', 'aksrt'), ['status' => 401]);
}
if ($password === '') {
return new WP_Error('aksrt_card_empty', __('请输入卡密', 'aksrt'), ['status' => 400]);
}
// Simple per-user throttle.
$throttle_key = 'aksrt_card_pass_rl_' . $user_id;
$hits = (int) get_transient($throttle_key);
if ($hits >= 20) {
return new WP_Error('aksrt_card_rate', __('操作过于频繁,请稍后再试', 'aksrt'), ['status' => 429]);
}
set_transient($throttle_key, $hits + 1, MINUTE_IN_SECONDS);
$row = $this->find_active($password, $card, $expected_type);
if (!$row) {
return new WP_Error('aksrt_card_invalid', __('卡号或卡密错误', 'aksrt'), ['status' => 400]);
}
$type = sanitize_key((string) ($row['type'] ?? self::TYPE_BALANCE));
if ($expected_type !== '' && $type !== $expected_type) {
return new WP_Error('aksrt_card_type', __('当前卡密不能用于该兑换类型', 'aksrt'), ['status' => 400]);
}
$gate = $this->assert_type_enabled($type);
if (is_wp_error($gate)) {
return $gate;
}
// only_password: if feature requires card number and empty, fail.
$opts = $this->get_public_options();
$only_password = true;
if ($type === self::TYPE_BALANCE) {
$only_password = !empty($opts['balance']['only_password']);
} elseif ($type === self::TYPE_VIP) {
$only_password = !empty($opts['vip']['only_password']);
}
if (!$only_password && $card === '' && (string) ($row['card'] ?? '') !== '') {
return new WP_Error('aksrt_card_need_no', __('请输入卡号', 'aksrt'), ['status' => 400]);
}
if (!$only_password && $card !== '' && (string) ($row['card'] ?? '') !== '' && (string) $row['card'] !== $card) {
return new WP_Error('aksrt_card_invalid', __('卡号或卡密错误', 'aksrt'), ['status' => 400]);
}
if (!empty($row['expires_at']) && strtotime((string) $row['expires_at']) < time()) {
global $wpdb;
$wpdb->update($this->table(), ['status' => self::STATUS_EXPIRED], ['id' => (int) $row['id']], ['%s'], ['%d']);
return new WP_Error('aksrt_card_expired', __('卡密已过期', 'aksrt'), ['status' => 400]);
}
$meta = $this->decode_meta($row);
$card_id = (int) $row['id'];
// Create audit order first (pending), then CAS mark used, then fulfill + mark paid.
$order_number = $this->create_redeem_order($user_id, $type, $meta, $card_id, $password);
if ($order_number === '') {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
if (!$this->mark_used_cas($card_id, $user_id, $order_number)) {
// Race lost.
if (class_exists('AKSRT_Order')) {
AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_CANCELLED, [
'note' => __('卡密已被使用,订单取消', 'aksrt'),
]);
}
return new WP_Error('aksrt_card_used', __('该卡密已使用', 'aksrt'), ['status' => 400]);
}
$fulfill = $this->fulfill($user_id, $type, $meta, $order_number, $card_id);
if (is_wp_error($fulfill)) {
return $fulfill;
}
if (class_exists('AKSRT_Order')) {
AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => 'card_pass',
'payment_transaction_id' => 'card_pass_' . $card_id,
'note' => (string) ($fulfill['message'] ?? __('卡密兑换成功', 'aksrt')),
]);
}
$out = array_merge([
'success' => true,
'type' => $type,
'order_number' => $order_number,
'reload' => true,
], $fulfill);
if (class_exists('AKSRT_Credits')) {
$out['balance'] = AKSRT_Credits::balance($user_id);
}
return $out;
}
/**
* @return array<string,mixed>|null
*/
public function find_active(string $password, string $card = '', string $type = ''): ?array {
global $wpdb;
$password = sanitize_text_field($password);
if ($password === '') {
return null;
}
$sql = "SELECT * FROM {$this->table()} WHERE password = %s";
$params = [$password];
if ($type !== '') {
$sql .= ' AND type = %s';
$params[] = sanitize_key($type);
}
$sql .= ' LIMIT 1';
$row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A);
if (!$row) {
return null;
}
if ((string) ($row['status'] ?? '') !== self::STATUS_ACTIVE) {
return null;
}
return $row;
}
public function mark_used_cas(int $card_id, int $user_id, string $order_number): bool {
global $wpdb;
if ($card_id <= 0) {
return false;
}
$updated = $wpdb->query($wpdb->prepare(
"UPDATE {$this->table()} SET status = %s, used_by = %d, used_at = %s, order_number = %s, balance = 0
WHERE id = %d AND status = %s",
self::STATUS_USED,
$user_id,
current_time('mysql'),
$order_number,
$card_id,
self::STATUS_ACTIVE
));
return $updated === 1;
}
/**
* @param array<string,mixed> $meta
* @return array{message:string,amount?:float,level?:int,days?:int}|WP_Error
*/
private function fulfill(int $user_id, string $type, array $meta, string $order_number, int $card_id) {
if ($type === self::TYPE_BALANCE) {
$amount = round((float) ($meta['amount'] ?? 0), 2);
if ($amount <= 0) {
return new WP_Error('aksrt_card_amount', __('当前卡密可充值金额为0', 'aksrt'), ['status' => 400]);
}
// Cash wallet first (Zibll pay_balance); fall back to points if wallet class missing.
if (class_exists('AKSRT_Balance')) {
$new = AKSRT_Balance::add($user_id, $amount, 'card_pass_balance', [
'type' => 'charge_card',
'id' => $card_id,
'order' => $order_number,
], sprintf(__('卡密充值 %s 元', 'aksrt'), (string) $amount));
if (is_wp_error($new)) {
return new WP_Error('aksrt_balance_fail', $new->get_error_message() ?: __('余额入账失败', 'aksrt'), ['status' => 500]);
}
return [
'message' => sprintf(__('卡密充值成功,充值金额:%s 元', 'aksrt'), (string) $amount),
'amount' => (float) $amount,
];
}
if (!class_exists('AKSRT_Credits')) {
return new WP_Error('aksrt_card_amount', __('余额系统不可用', 'aksrt'), ['status' => 500]);
}
$pts = (int) round($amount);
$new = AKSRT_Credits::add($user_id, $pts, 'card_pass_balance', [
'ref_type' => 'charge_card',
'ref_id' => $card_id,
'order' => $order_number,
], sprintf(__('卡密充值 %d', 'aksrt'), $pts));
if ($new === null || $new === false || is_wp_error($new)) {
return new WP_Error('aksrt_credit_fail', __('余额入账失败', 'aksrt'), ['status' => 500]);
}
return [
'message' => sprintf(__('卡密充值成功,充值金额:%s', 'aksrt'), (string) $pts),
'amount' => (float) $pts,
];
}
if ($type === self::TYPE_VIP) {
$level = max(1, min(2, (int) ($meta['level'] ?? 1)));
$days = max(0, (int) ($meta['days'] ?? 0));
if (!class_exists('AKSRT_Vip_Service')) {
return new WP_Error('aksrt_vip_unavailable', __('会员系统不可用', 'aksrt'), ['status' => 500]);
}
AKSRT_Vip_Service::get_instance()->activate_vip($user_id, $level, $days);
$msg = $days > 0
? sprintf(
/* translators: 1: level 2: days */
__('卡密兑换成功:VIP%s %d 天', 'aksrt'),
(string) $level,
$days
)
: sprintf(
/* translators: %s: level */
__('卡密兑换成功:VIP%s 永久', 'aksrt'),
(string) $level
);
return [
'message' => $msg,
'level' => $level,
'days' => $days,
];
}
if ($type === self::TYPE_CREDITS) {
$points = (int) ($meta['points'] ?? 0);
if ($points <= 0 || !class_exists('AKSRT_Credits')) {
return new WP_Error('aksrt_card_points', __('当前卡密可兑换积分为0', 'aksrt'), ['status' => 400]);
}
AKSRT_Credits::add($user_id, $points, 'card_pass_credits', [
'ref_type' => 'charge_card',
'ref_id' => $card_id,
'order' => $order_number,
], sprintf(__('卡密兑换积分 %d', 'aksrt'), $points));
return [
'message' => sprintf(__('卡密兑换成功,积分:%d', 'aksrt'), $points),
'amount' => (float) $points,
];
}
return new WP_Error('aksrt_card_type', __('不支持的卡密类型', 'aksrt'), ['status' => 400]);
}
/**
* @param array<string,mixed> $meta
*/
private function create_redeem_order(int $user_id, string $type, array $meta, int $card_id, string $password): string {
if (!class_exists('AKSRT_Order_Repository') || !class_exists('AKSRT_Domain_Model')) {
return 'CARD' . $card_id . '_' . time();
}
$product_type = AKSRT_Domain_Model::ORDER_PRODUCT_RECHARGE;
$product_id = $card_id;
$amount = 0.0;
$currency = 'CNY';
if ($type === self::TYPE_VIP) {
$product_type = AKSRT_Domain_Model::ORDER_PRODUCT_VIP;
$amount = 0.0;
} elseif ($type === self::TYPE_BALANCE) {
$amount = round((float) ($meta['amount'] ?? 0), 2);
$currency = 'CNY';
} elseif ($type === self::TYPE_CREDITS) {
$amount = (float) ($meta['points'] ?? 0);
$currency = 'points';
}
$note = wp_json_encode([
'card_pass' => 1,
'card_id' => $card_id,
'card_type' => $type,
'meta' => $meta,
'password' => substr(md5($password), 0, 8),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return AKSRT_Order_Repository::get_instance()->create([
'user_id' => $user_id,
'product_type' => $product_type,
'product_id' => $product_id,
'amount' => $amount,
'currency' => $currency,
'status' => class_exists('AKSRT_Order') ? AKSRT_Order::STATUS_PENDING : 'pending',
'payment_method' => 'card_pass',
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
}
/**
* @return true|WP_Error
*/
private function assert_type_enabled(string $type) {
$opts = $this->get_public_options();
if ($type === self::TYPE_BALANCE && empty($opts['balance']['enabled'])) {
return new WP_Error('aksrt_card_disabled', __('余额卡密兑换未开启', 'aksrt'), ['status' => 403]);
}
if ($type === self::TYPE_VIP && empty($opts['vip']['enabled'])) {
return new WP_Error('aksrt_card_disabled', __('会员卡密兑换未开启', 'aksrt'), ['status' => 403]);
}
if ($type === self::TYPE_CREDITS && empty($opts['credits']['enabled'])) {
return new WP_Error('aksrt_card_disabled', __('积分卡密兑换未开启', 'aksrt'), ['status' => 403]);
}
return true;
}
/**
* @param array<string,mixed> $data
* @return array<string,mixed>|null
*/
private function normalize_meta(string $type, array $data): ?array {
if (is_array($data['meta'] ?? null)) {
$raw = $data['meta'];
} else {
$raw = $data;
}
if ($type === self::TYPE_BALANCE) {
$amount = round((float) ($raw['amount'] ?? $data['amount'] ?? 0), 2);
if ($amount <= 0) {
return null;
}
return ['amount' => $amount];
}
if ($type === self::TYPE_VIP) {
$level = max(1, min(2, (int) ($raw['level'] ?? $data['level'] ?? 1)));
$days = max(0, (int) ($raw['days'] ?? $data['days'] ?? $data['vip_days'] ?? 0));
return ['level' => $level, 'days' => $days];
}
if ($type === self::TYPE_CREDITS) {
$points = max(0, (int) ($raw['points'] ?? $data['points'] ?? $data['credits'] ?? 0));
if ($points <= 0) {
return null;
}
return ['points' => $points];
}
return null;
}
/**
* @param array<string,mixed> $row
* @return array<string,mixed>
*/
private function decode_meta(array $row): array {
$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;
}
}
}
// Legacy amount-only rows.
if ($meta === [] && isset($row['amount'])) {
$meta = ['amount' => (float) $row['amount']];
}
return $meta;
}
/**
* Admin list helper (supports new + legacy columns).
*
* @return array{items:array,total:int,page:int,max_pages:int}
*/
public function get_list(array $args = []): array {
global $wpdb;
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$status = sanitize_key((string) ($args['status'] ?? ''));
$type = sanitize_key((string) ($args['type'] ?? ''));
$search = sanitize_text_field((string) ($args['search'] ?? $args['s'] ?? ''));
$where = [];
$params = [];
if ($status !== '') {
$where[] = 'status = %s';
$params[] = $status;
}
if ($type !== '') {
if ($type === 'points') {
$type = self::TYPE_CREDITS;
}
$where[] = 'type = %s';
$params[] = $type;
}
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)';
$params[] = $like;
$params[] = $like;
$params[] = $like;
$params[] = $search;
}
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
$total = $params
? (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";
$params[] = $limit;
$params[] = $offset;
$items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
return [
'items' => $items ?: [],
'total' => $total,
'page' => (int) floor($offset / $limit) + 1,
'max_pages' => (int) ceil(max(1, $total) / $limit),
];
}
public function disable(int $id): bool {
global $wpdb;
return (bool) $wpdb->update($this->table(), [
'status' => self::STATUS_DISABLED,
], ['id' => $id], ['%s'], ['%d']);
}
/**
* @return array<string,mixed>
*/
public function get_stats(): array {
global $wpdb;
$table = $this->table();
$row = $wpdb->get_row(
"SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
SUM(CASE WHEN status = 'used' THEN 1 ELSE 0 END) as used,
SUM(amount) as total_amount,
SUM(CASE WHEN status = 'used' THEN amount ELSE 0 END) as used_amount
FROM {$table}",
ARRAY_A
);
return [
'total' => (int) ($row['total'] ?? 0),
'active' => (int) ($row['active'] ?? 0),
'used' => (int) ($row['used'] ?? 0),
'total_amount' => (float) ($row['total_amount'] ?? 0),
'used_amount' => (float) ($row['used_amount'] ?? 0),
];
}
/**
* Legacy alias used by older admin code.
*
* @return array{success:bool,message:string,amount?:float}
*/
public function redeem(string $code, int $user_id): array {
$result = $this->redeem_for_user($user_id, $code, '', self::TYPE_BALANCE);
if (is_wp_error($result)) {
return ['success' => false, 'message' => $result->get_error_message()];
}
return [
'success' => true,
'message' => (string) ($result['message'] ?? __('兑换成功', 'aksrt')),
'amount' => (float) ($result['amount'] ?? 0),
];
}
}