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: '开启后只需输入卡密;关闭则需卡号+卡密。' },
],
},
{