feat(pay): card-pass redeem for VIP and balance (zibll-style)

Rewrite charge-card into typed single-use card_pass inventory with public redeem REST, user-center UI for balance/VIP, admin type-aware create/batch, and CAS mark-used fulfillment via Credits/activate_vip.
This commit is contained in:
2026-07-14 20:13:46 +08:00
parent 5d7b138c2c
commit d34c954b83
14 changed files with 936 additions and 132 deletions
@@ -51,21 +51,33 @@
<n-modal v-model:show="createVisible" preset="card" title="创建充值卡" style="max-width: 520px;">
<n-form :model="createForm" label-placement="top">
<n-form-item label="卡号">
<n-input v-model:value="createForm.code" placeholder="手动填写唯一卡号" />
<n-form-item label="卡类型">
<n-select v-model:value="createForm.type" :options="typeOptions" />
</n-form-item>
<n-grid cols="2" :x-gap="12">
<n-form-item label="卡密">
<n-input v-model:value="createForm.code" placeholder="手动填写唯一卡密" />
</n-form-item>
<n-form-item v-if="createForm.type === 'balance'" label="面额">
<n-input-number v-model:value="createForm.amount" :min="0.01" :precision="2" style="width: 100%;" />
</n-form-item>
<n-grid v-if="createForm.type === 'vip'" cols="2" :x-gap="12">
<n-grid-item>
<n-form-item label="面额">
<n-input-number v-model:value="createForm.amount" :min="0.01" :precision="2" style="width: 100%;" />
<n-form-item label="VIP 等级">
<n-select v-model:value="createForm.level" :options="levelOptions" />
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="过期时间">
<n-date-picker v-model:value="createForm.expiresAt" type="datetime" clearable style="width: 100%;" />
<n-form-item label="天数(0=永久)">
<n-input-number v-model:value="createForm.days" :min="0" :max="3650" style="width: 100%;" />
</n-form-item>
</n-grid-item>
</n-grid>
<n-form-item v-if="createForm.type === 'credits'" label="积分">
<n-input-number v-model:value="createForm.points" :min="1" style="width: 100%;" />
</n-form-item>
<n-form-item label="过期时间">
<n-date-picker v-model:value="createForm.expiresAt" type="datetime" clearable style="width: 100%;" />
</n-form-item>
<n-form-item label="备注">
<n-input v-model:value="createForm.note" type="textarea" :rows="3" />
</n-form-item>
@@ -78,22 +90,42 @@
<n-modal v-model:show="batchVisible" preset="card" title="批量生成充值卡" style="max-width: 520px;">
<n-form :model="batchForm" label-placement="top">
<n-form-item label="卡类型">
<n-select v-model:value="batchForm.type" :options="typeOptions" />
</n-form-item>
<n-grid cols="2" :x-gap="12">
<n-grid-item>
<n-form-item label="数量">
<n-input-number v-model:value="batchForm.count" :min="1" :max="100" style="width: 100%;" />
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-grid-item v-if="batchForm.type === 'balance'">
<n-form-item label="面额">
<n-input-number v-model:value="batchForm.amount" :min="0.01" :precision="2" style="width: 100%;" />
</n-form-item>
</n-grid-item>
<n-grid-item v-if="batchForm.type === 'credits'">
<n-form-item label="积分">
<n-input-number v-model:value="batchForm.points" :min="1" style="width: 100%;" />
</n-form-item>
</n-grid-item>
</n-grid>
<n-grid v-if="batchForm.type === 'vip'" cols="2" :x-gap="12">
<n-grid-item>
<n-form-item label="VIP 等级">
<n-select v-model:value="batchForm.level" :options="levelOptions" />
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="天数(0=永久)">
<n-input-number v-model:value="batchForm.days" :min="0" :max="3650" style="width: 100%;" />
</n-form-item>
</n-grid-item>
</n-grid>
<n-grid cols="2" :x-gap="12">
<n-grid-item>
<n-form-item label="前缀">
<n-input v-model:value="batchForm.prefix" placeholder="CARD" />
<n-input v-model:value="batchForm.prefix" placeholder="AK" />
</n-form-item>
</n-grid-item>
<n-grid-item>
@@ -152,21 +184,40 @@ const filters = reactive({
})
const createForm = reactive({
type: 'balance',
code: '',
amount: 10,
level: 1,
days: 30,
points: 100,
expiresAt: null,
note: '',
})
const batchForm = reactive({
type: 'balance',
count: 20,
amount: 10,
prefix: 'CARD',
level: 1,
days: 30,
points: 100,
prefix: 'AK',
length: 12,
expiresAt: null,
note: '',
})
const typeOptions = [
{ label: '余额卡', value: 'balance' },
{ label: '会员卡', value: 'vip' },
{ label: '积分卡', value: 'credits' },
]
const levelOptions = [
{ label: 'VIP1', value: 1 },
{ label: 'VIP2', value: 2 },
]
const pager = reactive({
page: 1,
pageSize: 20,
@@ -214,9 +265,26 @@ const pagination = computed(() => ({
const columns = [
{ title: 'ID', key: 'id', width: 70 },
{ title: '卡号', key: 'code', minWidth: 190, ellipsis: { tooltip: true } },
{ title: '面额', key: 'amount', width: 90 },
{ title: '余额', key: 'balance', width: 90 },
{ title: '卡密', key: 'code', minWidth: 170, ellipsis: { tooltip: true } },
{
title: '类型',
key: 'type_label',
width: 90,
render: (row) => row.type_label || row.type || '-',
},
{
title: '面额/权益',
key: 'amount',
width: 120,
render: (row) => {
if (row.type === 'vip') {
const days = Number(row.days || 0)
return `VIP${row.level || 1} / ${days > 0 ? `${days}天` : '永久'}`
}
if (row.type === 'credits') return `${row.points || row.amount || 0} 积分`
return row.amount
},
},
{
title: '状态',
key: 'status_label',
@@ -339,8 +407,13 @@ async function handleCreate() {
await api.request('/payment/charge-cards', {
method: 'POST',
data: {
type: createForm.type,
code: createForm.code,
password: createForm.code,
amount: createForm.amount,
level: createForm.level,
days: createForm.days,
points: createForm.points,
expires_at: createForm.expiresAt ? formatDateValue(createForm.expiresAt) : '',
note: createForm.note,
},
@@ -363,8 +436,12 @@ async function handleBatchCreate() {
await api.request('/payment/charge-cards/batch', {
method: 'POST',
data: {
type: batchForm.type,
count: batchForm.count,
amount: batchForm.amount,
level: batchForm.level,
days: batchForm.days,
points: batchForm.points,
prefix: batchForm.prefix,
length: batchForm.length,
expires_at: batchForm.expiresAt ? formatDateValue(batchForm.expiresAt) : '',
@@ -71,9 +71,9 @@ export const rechargeSchema = {
title: '充值卡免扣',
icon: 'fas fa-ticket-alt',
fields: [
{ key: 'pay_balance_pass_charge_s', label: '启用充值卡免扣', type: 'switch', default: false },
{ key: 'pay_balance_pass_charge_desc', label: '免扣说明', type: 'textarea', default: '', dependsOn: { key: 'pay_balance_pass_charge_s', value: true } },
{ key: 'pay_balance_pass_charge_only_password', label: '仅密码免扣', type: 'switch', default: false, dependsOn: { key: 'pay_balance_pass_charge_s', value: true } },
{ key: 'pay_balance_pass_charge_s', label: '启用余额卡密兑换', type: 'switch', default: false },
{ key: 'pay_balance_pass_charge_desc', label: '卡密兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_balance_pass_charge_s', value: true } },
{ key: 'pay_balance_pass_charge_only_password', label: '仅密码兑换', type: 'switch', default: false, dependsOn: { key: 'pay_balance_pass_charge_s', value: true } },
],
},
{
@@ -18,6 +18,7 @@ export const vipSchema = {
pay_download_speed_vip_2: 0,
pay_vip_pass_charge_s: false,
pay_vip_pass_charge_desc: '',
pay_vip_pass_charge_only_password: true,
vip_renew: true,
vip_renew_desc: '',
vip_renew_price_type: 'discount',
@@ -61,6 +62,7 @@ export const vipSchema = {
{ key: 'pay_download_speed_vip_2', label: '二级会员下载速度(KB/s)', type: 'number', default: 0, min: 0, max: 99999, dependsOn: { key: 'pay_user_vip_2_s', value: true } },
{ key: 'pay_vip_pass_charge_s', label: '会员卡密兑换', type: 'switch', default: false },
{ key: 'pay_vip_pass_charge_desc', label: '卡密兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_vip_pass_charge_s', value: true } },
{ key: 'pay_vip_pass_charge_only_password', label: '仅密码兑换', type: 'switch', default: true, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '开启后只需输入卡密;关闭则需卡号+卡密。' },
],
},
{
+59
View File
@@ -560,3 +560,62 @@
box-shadow: none;
border-color: var(--color-line, #e5e7eb);
}
/* Card-pass redeem (balance / VIP) */
.uc-card-pass {
margin: 16px 0 0;
padding: 14px 16px;
border: 1px solid var(--color-line, #e5e7eb);
border-radius: 12px;
background: #fff;
}
.uc-card-pass__title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 700;
color: #111827;
margin: 0 0 8px;
}
.uc-card-pass__title i { color: #2563eb; }
.uc-card-pass__desc {
margin: 0 0 10px;
font-size: 12px;
color: #6b7280;
line-height: 1.5;
}
.uc-card-pass__input {
display: block;
width: 100%;
margin: 0 0 8px;
padding: 9px 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 13px;
box-sizing: border-box;
}
.uc-card-pass__btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
padding: 0 14px;
border: 0;
border-radius: 8px;
background: #2563eb;
color: #fff;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.uc-card-pass__btn:hover { background: #1d4ed8; }
.uc-card-pass__btn:disabled { opacity: .6; cursor: not-allowed; }
.uc-card-pass__tip {
margin: 8px 0 0;
font-size: 12px;
color: #059669;
min-height: 1.2em;
}
.uc-card-pass__tip.is-error { color: #b91c1c; }
+68 -1
View File
@@ -442,17 +442,84 @@
});
}
// ===== 卡密兑换(余额 / 会员) =====
function initCardPass() {
var roots = document.querySelectorAll('[data-aksrt-card-pass]');
if (!roots || !roots.length) return;
Array.prototype.forEach.call(roots, function (root) {
var tip = root.querySelector('[data-aksrt-card-tip]');
var busy = false;
function setTip(text, isError) {
if (!tip) return;
tip.textContent = text || '';
tip.classList.toggle('is-error', !!isError);
}
var btn = root.querySelector('[data-aksrt-card-redeem]');
if (!btn) return;
btn.addEventListener('click', function () {
if (busy || btn.disabled) return;
var type = btn.getAttribute('data-type') || root.getAttribute('data-aksrt-card-pass') || '';
var pwInput = root.querySelector('[data-aksrt-card-password]');
var cardInput = root.querySelector('[data-aksrt-card-no]');
var password = pwInput ? String(pwInput.value || '').trim() : '';
var card = cardInput ? String(cardInput.value || '').trim() : '';
if (!password) {
setTip('请输入卡密', true);
if (pwInput) pwInput.focus();
return;
}
if (cardInput && !card) {
setTip('请输入卡号', true);
cardInput.focus();
return;
}
busy = true;
btn.disabled = true;
setTip('正在兑换…');
var body = { password: password, type: type };
if (card) body.card = card;
api('/pay/card-pass/redeem', {
method: 'POST',
body: JSON.stringify(body)
}).then(function (r) {
busy = false;
btn.disabled = false;
if (!r.ok || (r.json && r.json.code && !r.json.success)) {
setTip((r.json && r.json.message) || '兑换失败', true);
return;
}
var data = (r.json && r.json.data) ? r.json.data : r.json;
setTip((data && data.message) || '兑换成功,正在刷新…');
window.location.reload();
}).catch(function () {
busy = false;
btn.disabled = false;
setTip('网络错误,请重试', true);
});
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
initSignin();
initInvite();
initVipShop();
initVipPointsExchange();
initVipPointsExchange();
initCardPass();
});
} else {
initSignin();
initInvite();
initVipShop();
initVipPointsExchange();
initCardPass();
}
})();
+3
View File
@@ -180,6 +180,9 @@ if (class_exists('AKSRT_Content_Pay')) {
if (class_exists('AKSRT_Vip_Service')) {
AKSRT_Vip_Service::get_instance()->init();
}
if (class_exists('AKSRT_Charge_Card')) {
AKSRT_Charge_Card::get_instance()->init();
}
// Download State & Docs Presenter are pure service singletons (no hooks)
if (class_exists('AKSRT_Download_State')) {
AKSRT_Download_State::get_instance();
@@ -334,6 +334,9 @@ class AKSRT_Admin_Payment_Controller extends AKSRT_Admin_Controller_Base {
$item = $this->build_charge_card_admin_item($row);
if ($keyword !== '' && mb_stripos(implode(' ', array_filter([
(string) ($item['code'] ?? ''),
(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'] ?? ''),
@@ -377,14 +380,19 @@ class AKSRT_Admin_Payment_Controller extends AKSRT_Admin_Controller_Base {
}
$params = $this->get_json_params($request);
$code = sanitize_text_field((string) ($params['code'] ?? ''));
$amount = (float) ($params['amount'] ?? 0);
if ($code === '' || $amount <= 0) {
return $this->error('卡号和金额不能为空');
$type = sanitize_key((string) ($params['type'] ?? 'balance'));
if (!in_array($type, ['balance', 'vip', 'credits'], true)) {
$type = 'balance';
}
$password = sanitize_text_field((string) ($params['password'] ?? $params['code'] ?? ''));
if ($password === '') {
return $this->error('卡密不能为空');
}
$id = AKSRT_Charge_Card::get_instance()->create($params);
$id = AKSRT_Charge_Card::get_instance()->create(array_merge($params, [
'type' => $type,
'password' => $password,
]));
if ($id > 0) {
return $this->success([
@@ -393,7 +401,7 @@ class AKSRT_Admin_Payment_Controller extends AKSRT_Admin_Controller_Base {
], 201);
}
return $this->error(__('创建失败(可能卡号已存在)', 'aksrt'));
return $this->error(__('创建失败(可能卡密已存在或参数无效)', 'aksrt'));
}
/**
@@ -405,35 +413,43 @@ class AKSRT_Admin_Payment_Controller extends AKSRT_Admin_Controller_Base {
}
$params = $this->get_json_params($request);
$amount = (float) ($params['amount'] ?? 0);
$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'] ?? ''));
if ($amount <= 0) {
return $this->error('金额必须大于 0');
$meta_fields = [
'amount' => (float) ($params['amount'] ?? 0),
'level' => (int) ($params['level'] ?? 1),
'days' => (int) ($params['days'] ?? $params['vip_days'] ?? 30),
'points' => (int) ($params['points'] ?? $params['credits'] ?? 0),
];
if ($type === 'balance' && $meta_fields['amount'] <= 0) {
return $this->error('余额卡金额必须大于 0');
}
if ($type === 'credits' && $meta_fields['points'] <= 0) {
return $this->error('积分卡积分必须大于 0');
}
$service = AKSRT_Charge_Card::get_instance();
$codes = [];
for ($i = 0; $i < $count; $i++) {
$code = $prefix . strtoupper(wp_generate_password($length, false));
$id = $service->create([
'code' => $code,
'amount' => $amount,
'expires_at' => $expires,
'note' => $note,
]);
if ($id > 0) {
$codes[] = $code;
}
}
$codes = AKSRT_Charge_Card::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)],
'data' => ['codes' => $codes, 'count' => count($codes), 'type' => $type],
]);
}
@@ -650,18 +666,48 @@ class AKSRT_Admin_Payment_Controller extends AKSRT_Admin_Controller_Base {
$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'] ?? $row['code'] ?? '');
$card_no = (string) ($row['card'] ?? '');
return [
'id' => (int) ($row['id'] ?? 0),
'code' => (string) ($row['code'] ?? ''),
'code' => $password,
'password' => $password,
'card' => $card_no,
'type' => $type,
'type_label' => $typeMap[$type] ?? $type,
'amount' => (float) ($row['amount'] ?? 0),
'balance' => (float) ($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,
@@ -86,9 +86,9 @@ class AKSRT_Admin_Schema_Payment {
['key' => 'pay_balance_transfer_desc', 'label' => '转账说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_transfer_s', 'value' => true]],
]],
['key' => 'pass-charge', 'title' => '充值卡免扣', 'fields' => [
['key' => 'pay_balance_pass_charge_s', 'label' => '启用充值卡免扣', 'type' => 'switch', 'default' => false],
['key' => 'pay_balance_pass_charge_desc', 'label' => '免扣说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true]],
['key' => 'pay_balance_pass_charge_only_password', 'label' => '仅密码免扣', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true]],
['key' => 'pay_balance_pass_charge_s', 'label' => '启用余额卡密兑换', 'type' => 'switch', 'default' => false],
['key' => 'pay_balance_pass_charge_desc', 'label' => '卡密兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true]],
['key' => 'pay_balance_pass_charge_only_password', 'label' => '仅密码兑换', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true]],
]],
['key' => 'exchange', 'title' => '积分兑换', 'fields' => [
['key' => 'pay_balance_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false],
@@ -127,6 +127,7 @@ class AKSRT_Admin_Schema_Payment {
['key' => 'pay_download_speed_vip_2', 'label' => '二级会员下载速度(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
['key' => 'pay_vip_pass_charge_s', 'label' => '会员卡密兑换', 'type' => 'switch', 'default' => false],
['key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true]],
['key' => 'pay_vip_pass_charge_only_password', 'label' => '仅密码兑换', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true]],
]],
['key' => 'renew', 'title' => '续费设置', 'fields' => [
['key' => 'vip_renew', 'label' => '允许续费', 'type' => 'switch', 'default' => true],
+556 -81
View File
@@ -1,8 +1,9 @@
<?php
/**
* AKSRT Charge Card — 充值卡系统
* AKSRT Charge Card — 卡密(余额 / VIP / 积分)
*
* 管理充值卡的创建、使用和查询。
* zibll-style card_pass: typed inventory, single-use CAS redeem,
* public REST for user-center, admin create/batch.
*
* @package AKSRT
*/
@@ -13,10 +14,14 @@ class AKSRT_Charge_Card {
private static $instance = null;
const STATUS_ACTIVE = 'active';
const STATUS_USED = 'used';
const STATUS_EXPIRED = 'expired';
const STATUS_DISABLED = 'disabled';
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) {
@@ -25,116 +30,568 @@ class AKSRT_Charge_Card {
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']),
];
}
}
return [
'balance' => $balance,
'vip' => $vip,
'credits' => [
'enabled' => false,
'desc' => '',
'only_password' => true,
],
'labels' => [
'title' => __('卡密兑换', 'aksrt'),
'password' => __('卡密', 'aksrt'),
'card' => __('卡号', 'aksrt'),
'submit_balance' => __('兑换余额', 'aksrt'),
'submit_vip' => __('兑换会员', '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;
$code = sanitize_text_field((string) ($data['code'] ?? ''));
$amount = (float) ($data['amount'] ?? 0);
$expires_at = sanitize_text_field((string) ($data['expires_at'] ?? ''));
$note = sanitize_textarea_field((string) ($data['note'] ?? ''));
$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;
}
if ($code === '' || $amount <= 0) {
$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;
}
// 检查 code 唯一性
// Uniqueness: password alone (only_password mode common).
$exists = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$this->table()} WHERE code = %s",
$code
"SELECT id FROM {$this->table()} WHERE password = %s LIMIT 1",
$password
));
if ($exists) return 0;
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(), [
'code' => $code,
'amount' => $amount,
'balance' => $amount,
'status' => self::STATUS_ACTIVE,
'expires_at' => $expires_at !== '' ? $expires_at : null,
'created_at' => current_time('mysql'),
'note' => $note,
'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(float $amount, int $count, int $length = 16, string $prefix = ''): array {
$codes = [];
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++) {
$code = $prefix . strtoupper(wp_generate_password($length, false));
$id = $this->create([
'code' => $code,
'amount' => $amount,
]);
$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[] = $code;
$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(string $code, int $user_id): array {
global $wpdb;
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);
$code = sanitize_text_field($code);
$card = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$this->table()} WHERE code = %s",
$code
), ARRAY_A);
if (!$card) {
return ['success' => false, 'message' => '充值卡不存在'];
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]);
}
if ($card['status'] !== self::STATUS_ACTIVE) {
return ['success' => false, 'message' => '充值卡已使用或已失效'];
// 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]);
}
if ($card['expires_at'] && strtotime($card['expires_at']) < time()) {
// 标记过期
$wpdb->update($this->table(), ['status' => self::STATUS_EXPIRED], ['id' => (int) $card['id']]);
return ['success' => false, 'message' => '充值卡已过期'];
$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]);
}
$amount = (float) $card['balance'];
$gate = $this->assert_type_enabled($type);
if (is_wp_error($gate)) {
return $gate;
}
// 更新卡状态
$wpdb->update($this->table(), [
'status' => self::STATUS_USED,
'used_by' => $user_id,
'used_at' => current_time('mysql'),
'balance' => 0,
], ['id' => (int) $card['id']]);
// 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 (class_exists('AKSRT_Credits')) {
AKSRT_Credits::add($user_id, (int) $amount, 'charge_card', [
'ref_type' => 'charge_card',
'ref_id' => (int) $card['id'],
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')),
]);
}
return [
'success' => true,
'message' => sprintf('充值成功,已到账 %s 元', number_format($amount, 2)),
'amount' => $amount,
];
$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 = (int) round((float) ($meta['amount'] ?? 0));
if ($amount <= 0 || !class_exists('AKSRT_Credits')) {
return new WP_Error('aksrt_card_amount', __('当前卡密可充值金额为0', 'aksrt'), ['status' => 400]);
}
$new = AKSRT_Credits::add($user_id, $amount, 'card_pass_balance', [
'ref_type' => 'charge_card',
'ref_id' => $card_id,
'order' => $order_number,
], sprintf(__('卡密充值 %d', 'aksrt'), $amount));
if ($new === null || $new === false) {
return new WP_Error('aksrt_credit_fail', __('余额入账失败', 'aksrt'), ['status' => 500]);
}
return [
'message' => sprintf(__('卡密充值成功,充值金额:%s', 'aksrt'), (string) $amount),
'amount' => (float) $amount,
];
}
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;
@@ -142,46 +599,47 @@ class AKSRT_Charge_Card {
$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'] ?? ''));
$where = [];
$where = [];
$params = [];
if ($status !== '') {
$where[] = 'status = %s';
$where[] = 'status = %s';
$params[] = $status;
}
if ($type !== '') {
$where[] = 'type = %s';
$params[] = $type;
}
$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);
$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' => floor($offset / $limit) + 1,
'max_pages' => ceil($total / $limit),
'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]);
], ['id' => $id], ['%s'], ['%d']);
}
/**
* 获取统计
* @return array<string,mixed>
*/
public function get_stats(): array {
global $wpdb;
@@ -206,4 +664,21 @@ class AKSRT_Charge_Card {
'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),
];
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ class AKSRT_DB_Migrator {
use AKSRT_DB_Migrator_Part_3;
/** Current schema version; changing it re-runs dbDelta. */
const SCHEMA_VERSION = '1.13.0';
const SCHEMA_VERSION = '1.14.0';
const OPT_KEY = 'aksrt_db_schema_version';
@@ -348,22 +348,29 @@ trait AKSRT_DB_Migrator_Part_2 {
) {$charset_collate};";
dbDelta($sql_shipping);
// ── charge_card ──
// ── charge_card (zibll-style typed card_pass) ──
$sql_charge_card = "CREATE TABLE {$prefix}aksrt_charge_card (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(60) NOT NULL DEFAULT '',
card VARCHAR(60) NOT NULL DEFAULT '',
password VARCHAR(80) NOT NULL DEFAULT '',
type VARCHAR(20) NOT NULL DEFAULT 'balance',
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
balance DECIMAL(10,2) NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'active',
meta LONGTEXT DEFAULT NULL,
order_number VARCHAR(32) NOT NULL DEFAULT '',
used_by BIGINT UNSIGNED DEFAULT NULL,
used_at DATETIME DEFAULT NULL,
expires_at DATETIME DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
note TEXT DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY idx_code (code),
UNIQUE KEY idx_password (password),
KEY idx_card (card),
KEY idx_type (type),
KEY idx_status (status),
KEY idx_used_by (used_by),
KEY idx_order_number (order_number),
KEY idx_expires (expires_at)
) {$charset_collate};";
dbDelta($sql_charge_card);
@@ -196,6 +196,7 @@ class AKSRT_Settings_Payment_Runtime {
'download_speed' => max(0, min(99999, (int) ($settings['pay_download_speed'] ?? 0))),
'pass_charge' => !empty($settings['pay_vip_pass_charge_s']),
'pass_charge_desc' => sanitize_textarea_field((string) ($settings['pay_vip_pass_charge_desc'] ?? '')),
'pass_charge_only_password' => !isset($settings['pay_vip_pass_charge_only_password']) || !empty($settings['pay_vip_pass_charge_only_password']),
],
'renew' => [
'enabled' => !isset($settings['vip_renew']) || !empty($settings['vip_renew']),
@@ -1,6 +1,6 @@
<?php
/**
* User center credits: balance card + sign-in controls.
* User center credits: balance card + sign-in controls + card-pass redeem.
*
* Expects extracted $context vars.
*
@@ -11,6 +11,20 @@ defined('ABSPATH') || exit;
$balance = (float) ($credit_balance ?? 0.0);
$credits_enabled = !empty($credits_enabled);
$card_pass_balance = ['enabled' => false, 'desc' => '', 'only_password' => true];
if (class_exists('AKSRT_Charge_Card')) {
$opts = AKSRT_Charge_Card::get_instance()->get_public_options();
$card_pass_balance = is_array($opts['balance'] ?? null) ? $opts['balance'] : $card_pass_balance;
} elseif (class_exists('AKSRT_Settings') && 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'] : [];
$card_pass_balance = [
'enabled' => !empty($pc['enabled']),
'desc' => (string) ($pc['desc'] ?? ''),
'only_password' => !isset($pc['only_password']) || !empty($pc['only_password']),
];
}
?>
<div class="uc-balance-card">
<div class="uc-balance-card__bg"></div>
@@ -34,3 +48,23 @@ $credits_enabled = !empty($credits_enabled);
<?php endif; ?>
</div>
</div>
<?php if (!empty($card_pass_balance['enabled'])) : ?>
<div class="uc-card-pass" data-aksrt-card-pass="balance">
<div class="uc-card-pass__title">
<i class="fas fa-ticket-alt" aria-hidden="true"></i>
<span><?php esc_html_e('卡密兑换余额', 'aksrt'); ?></span>
</div>
<?php if (!empty($card_pass_balance['desc'])) : ?>
<p class="uc-card-pass__desc"><?php echo esc_html((string) $card_pass_balance['desc']); ?></p>
<?php endif; ?>
<?php if (empty($card_pass_balance['only_password'])) : ?>
<input type="text" class="uc-card-pass__input" name="aksrt_card_no" data-aksrt-card-no autocomplete="off" placeholder="<?php esc_attr_e('请输入卡号', 'aksrt'); ?>">
<?php endif; ?>
<input type="text" class="uc-card-pass__input" name="aksrt_card_password" data-aksrt-card-password autocomplete="off" placeholder="<?php esc_attr_e('请输入卡密', 'aksrt'); ?>">
<button type="button" class="uc-card-pass__btn" data-aksrt-card-redeem data-type="balance">
<?php esc_html_e('兑换余额', 'aksrt'); ?>
</button>
<p class="uc-card-pass__tip" data-aksrt-card-tip></p>
</div>
<?php endif; ?>
@@ -70,6 +70,18 @@ if ($vip_level > 0) {
$shop_title = __('续费 / 升级会员', 'aksrt');
}
$points_pkgs = is_array($points_exchange['packages'] ?? null) ? $points_exchange['packages'] : [];
$card_pass_vip = ['enabled' => false, 'desc' => '', 'only_password' => true];
if (class_exists('AKSRT_Charge_Card')) {
$card_opts = AKSRT_Charge_Card::get_instance()->get_public_options();
$card_pass_vip = is_array($card_opts['vip'] ?? null) ? $card_opts['vip'] : $card_pass_vip;
} elseif (!empty($vip_settings['benefit']['pass_charge'])) {
$card_pass_vip = [
'enabled' => true,
'desc' => (string) ($vip_settings['benefit']['pass_charge_desc'] ?? ''),
'only_password' => !isset($vip_settings['benefit']['pass_charge_only_password']) || !empty($vip_settings['benefit']['pass_charge_only_password']),
];
}
?>
<div class="uc-vip-card<?php echo $is_vip_user ? ' uc-vip-card--active' : ''; ?>" data-aksrt-vip-panel>
<div class="uc-vip-card__bg"></div>
@@ -241,3 +253,23 @@ $points_pkgs = is_array($points_exchange['packages'] ?? null) ? $points_exchange
<?php elseif (empty($packages) && !empty($vip_settings['help_text'])) : ?>
<p class="uc-vip-shop__empty"><?php echo esc_html((string) $vip_settings['help_text']); ?></p>
<?php endif; ?>
<?php if (!empty($card_pass_vip['enabled'])) : ?>
<div class="uc-card-pass" data-aksrt-card-pass="vip">
<div class="uc-card-pass__title">
<i class="fas fa-ticket-alt" aria-hidden="true"></i>
<span><?php esc_html_e('卡密兑换会员', 'aksrt'); ?></span>
</div>
<?php if (!empty($card_pass_vip['desc'])) : ?>
<p class="uc-card-pass__desc"><?php echo esc_html((string) $card_pass_vip['desc']); ?></p>
<?php endif; ?>
<?php if (empty($card_pass_vip['only_password'])) : ?>
<input type="text" class="uc-card-pass__input" name="aksrt_card_no" data-aksrt-card-no autocomplete="off" placeholder="<?php esc_attr_e('请输入卡号', 'aksrt'); ?>">
<?php endif; ?>
<input type="text" class="uc-card-pass__input" name="aksrt_card_password" data-aksrt-card-password autocomplete="off" placeholder="<?php esc_attr_e('请输入卡密', 'aksrt'); ?>">
<button type="button" class="uc-card-pass__btn" data-aksrt-card-redeem data-type="vip">
<?php esc_html_e('兑换会员', 'aksrt'); ?>
</button>
<p class="uc-card-pass__tip" data-aksrt-card-tip></p>
</div>
<?php endif; ?>