积分与余额账务系统硬切重构

This commit is contained in:
2026-08-24 15:59:11 +08:00
parent ca12226b39
commit 5cae5b5c93
159 changed files with 7275 additions and 4718 deletions
+30
View File
@@ -12,6 +12,21 @@ import { apiMethods } from './modules/index.js'
const API_ROOT = window.aksrtAdminData?.apiUrl || '/wp-json/aksrt/v1'
const NONCE = window.aksrtAdminData?.nonce || ''
function createIdempotencyKey() {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
}
if (typeof crypto === 'undefined' || typeof crypto.getRandomValues !== 'function') {
throw new Error('当前浏览器无法生成安全操作标识')
}
const bytes = new Uint8Array(16)
crypto.getRandomValues(bytes)
bytes[6] = (bytes[6] & 0x0f) | 0x40
bytes[8] = (bytes[8] & 0x3f) | 0x80
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
}
function normalizeDomainKey(domain) {
return String(domain || 'global').trim().replace(/-/g, '_')
}
@@ -85,6 +100,7 @@ function joinRestUrl(baseUrl, endpoint) {
class ApiClient {
constructor() {
this.baseUrl = API_ROOT
this.pendingIdempotencyKeys = new Map()
}
normalizeDomainKey(domain) {
@@ -138,11 +154,22 @@ class ApiClient {
finalBody = JSON.stringify(body)
}
const isWrite = method !== 'GET' && method !== 'HEAD'
const suppliedKey = headers?.['Idempotency-Key'] || headers?.['idempotency-key'] || ''
const idempotencySignature = isWrite ? `${method}:${url}:${finalBody ?? ''}` : ''
const idempotencyKey = isWrite && !suppliedKey
? (this.pendingIdempotencyKeys.get(idempotencySignature) || createIdempotencyKey())
: suppliedKey
if (isWrite && !suppliedKey) {
this.pendingIdempotencyKeys.set(idempotencySignature, idempotencyKey)
}
const config = {
method,
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': NONCE,
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
...headers,
},
...rest,
@@ -174,6 +201,9 @@ class ApiClient {
throw new Error(msg)
}
if (idempotencySignature !== '') {
this.pendingIdempotencyKeys.delete(idempotencySignature)
}
return data
} catch (error) {
console.error(`[API Error] ${endpoint}:`, error)
+6 -6
View File
@@ -28,17 +28,17 @@ export const usersApiMethods = {
return this.request('/identity/settings/test', { method: 'POST', body: JSON.stringify(data) })
},
getCreditSettings() { return this.request('/credits/settings') },
saveCreditSettings(data) { return this.request('/credits/settings', { method: 'POST', body: JSON.stringify(data) }) },
getCreditSettings() { return this.request('/assets/settings') },
saveCreditSettings(data) { return this.request('/assets/settings', { method: 'POST', body: JSON.stringify(data) }) },
listCreditUsers(params = {}) {
const qs = new URLSearchParams(params).toString()
return this.request('/credits/users' + (qs ? `?${qs}` : ''))
return this.request('/assets/accounts' + (qs ? `?${qs}` : ''))
},
adjustUserCredits(id, delta, reason) {
return this.request(`/credits/users/${id}/adjust`, { method: 'POST', body: JSON.stringify({ delta, reason }) })
adjustUserCredits(id, asset, delta, remark) {
return this.request(`/assets/accounts/${id}/adjust`, { method: 'POST', body: JSON.stringify({ asset, delta, remark }) })
},
getCreditLog(params = {}) {
const qs = new URLSearchParams(params).toString()
return this.request('/credits/log' + (qs ? `?${qs}` : ''))
return this.request('/assets/transactions' + (qs ? `?${qs}` : ''))
},
}
+113 -5
View File
@@ -1,8 +1,8 @@
<template>
<div class="aksrt-page-container">
<h2 class=”aksrt-page-title”>积分配置</h2>
<p class=”aksrt-page-desc”>
配置积分规则与发放策略。可自定义货币名称(如”金币”、”魔石”等)。用户余额与手工调整已迁移到 WordPress 原生”用户”菜单中管理。
<h2 class="aksrt-page-title">积分配置</h2>
<p class="aksrt-page-desc">
配置积分规则与发放策略。可自定义货币名称(如”金币”、”魔石”等)。用户余额与手工调整在 WordPress 原生”用户”菜单中管理。
</p>
<n-card class="aksrt-section-card">
@@ -23,6 +23,35 @@
</n-form>
</n-card>
<n-card title="统一资产账本" class="aksrt-section-card">
<n-space vertical :size="12">
<n-space wrap>
<n-select v-model:value="ledgerFilters.asset" :options="assetOptions" clearable placeholder="资产" style="width: 130px" />
<n-input v-model:value="ledgerFilters.transaction_id" placeholder="交易号" clearable style="width: 130px" />
<n-input v-model:value="ledgerFilters.operation_key" placeholder="幂等键" clearable style="width: 240px" />
<n-input v-model:value="ledgerFilters.ref_type" placeholder="引用类型" clearable style="width: 130px" />
<n-input v-model:value="ledgerFilters.ref_key" placeholder="引用键" clearable style="width: 180px" />
<n-input v-model:value="ledgerFilters.actor_user_id" placeholder="操作者 ID" clearable style="width: 130px" />
<n-button type="primary" :loading="ledgerLoading" @click="searchLedger">查询</n-button>
<n-button @click="resetLedger">重置</n-button>
</n-space>
<n-data-table
:columns="ledgerColumns"
:data="ledgerRows"
:loading="ledgerLoading"
:row-key="row => row.id"
:scroll-x="1600"
size="small"
/>
<n-pagination
v-model:page="ledgerPage"
:page-count="ledgerPageCount"
:page-size="20"
@update:page="loadLedger"
/>
</n-space>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>每日签到</h3></div>
<n-form :model="cfg.daily_signin" label-placement="left" label-width="200" class="aksrt-form">
@@ -97,7 +126,7 @@
<n-form-item :label="`默认积分价参考(${cfg.currency_name || '积分'})`">
<n-input-number v-model:value="cfg.download_default_cost" :min="0" :max="100000" />
<template #feedback>
<span class="aksrt-hint-text">兼容字段。实际价格以文章「付费内容设置」的积分价/现金价为准;下载解锁走收银台订单,不按次扣费。</span>
<span class="aksrt-hint-text">实际价格以文章「付费内容设置」的积分价或现金价为准;下载解锁走收银台订单,不按次扣费。</span>
</template>
</n-form-item>
</n-form>
@@ -135,10 +164,88 @@ const cfg = reactive({
})
const streakJson = ref('{}')
const saving = ref(false)
const ledgerLoading = ref(false)
const ledgerRows = ref([])
const ledgerPage = ref(1)
const ledgerPageCount = ref(1)
const ledgerFilters = reactive({
asset: null,
transaction_id: '',
operation_key: '',
ref_type: '',
ref_key: '',
actor_user_id: '',
})
const assetOptions = [
{ label: '积分', value: 'points' },
{ label: '余额', value: 'balance' },
]
const ledgerColumns = [
{ title: '交易号', key: 'transaction_label', width: 100, fixed: 'left' },
{ title: '幂等键', key: 'operation_key', width: 260, ellipsis: { tooltip: true } },
{ title: '资产', key: 'assets_label', width: 100 },
{ title: '原因', key: 'reason', width: 150, ellipsis: { tooltip: true } },
{ title: '业务引用', key: 'reference_label', width: 200, ellipsis: { tooltip: true } },
{ title: '操作者', key: 'actor_label', width: 100 },
{ title: '增减 / 手续费', key: 'delta_label', width: 180, ellipsis: { tooltip: true } },
{ title: '变动后余额', key: 'balance_label', width: 150, ellipsis: { tooltip: true } },
{ title: '备注', key: 'remark', width: 180, ellipsis: { tooltip: true } },
{ title: 'UTC 时间', key: 'created_at', width: 190 },
]
function formatAssetValue(asset, value) {
if (asset === 'balance') return typeof value === 'string' && /^[+-]?\d+\.\d{2}$/.test(value) ? value : '0.00'
const raw = String(value ?? '0')
return /^-?\d+$/.test(raw) ? raw : '0'
}
async function loadLedger() {
ledgerLoading.value = true
try {
const params = { page: ledgerPage.value, per_page: 20 }
Object.entries(ledgerFilters).forEach(([key, value]) => {
if (value !== null && String(value).trim() !== '') params[key] = value
})
const response = await api.getCreditLog(params)
const data = response?.data || response || {}
ledgerRows.value = (data.items || []).map((row) => {
const entries = Array.isArray(row.entries) ? row.entries : []
return {
...row,
transaction_label: `#${row.id}`,
assets_label: (row.assets || []).join(' / '),
reference_label: [row.ref_type, row.ref_key].filter(Boolean).join(':') || '—',
actor_label: row.actor_user_id ? `#${row.actor_user_id}` : '系统',
delta_label: entries.map((entry) => {
const value = formatAssetValue(entry.asset, entry.delta)
const signed = entry.asset === 'points' && !value.startsWith('-') && value !== '0' ? `+${value}` : value
return `${entry.asset}:${signed}`
}).join(';'),
balance_label: entries.map((entry) => `${entry.asset}:${formatAssetValue(entry.asset, entry.balance_after)}`).join(';'),
}
})
ledgerPageCount.value = Math.max(1, Math.ceil(Number(data.total || 0) / Number(data.per_page || 20)))
} catch (e) {
message.error(e.message || '加载账本失败')
} finally {
ledgerLoading.value = false
}
}
function searchLedger() {
ledgerPage.value = 1
loadLedger()
}
function resetLedger() {
Object.assign(ledgerFilters, { asset: null, transaction_id: '', operation_key: '', ref_type: '', ref_key: '', actor_user_id: '' })
searchLedger()
}
async function loadCfg() {
try {
const data = await api.getCreditSettings()
const response = await api.getCreditSettings()
const data = response?.data || response || {}
Object.assign(cfg, data)
streakJson.value = JSON.stringify(cfg.daily_signin?.streak || {}, null, 0)
} catch (e) {
@@ -163,5 +270,6 @@ async function saveCfg() {
onMounted(() => {
loadCfg()
loadLedger()
})
</script>
@@ -22,10 +22,16 @@
<n-switch v-model:value="settings.enabled_types.download_ready" />
</n-form-item>
<n-form-item label="积分增加通知">
<n-switch v-model:value="settings.enabled_types.credits_added" />
<n-switch v-model:value="settings.enabled_types.asset_points_increase" />
</n-form-item>
<n-form-item label="积分扣减通知">
<n-switch v-model:value="settings.enabled_types.credits_deducted" />
<n-switch v-model:value="settings.enabled_types.asset_points_decrease" />
</n-form-item>
<n-form-item label="余额增加通知">
<n-switch v-model:value="settings.enabled_types.asset_balance_increase" />
</n-form-item>
<n-form-item label="余额扣减通知">
<n-switch v-model:value="settings.enabled_types.asset_balance_decrease" />
</n-form-item>
<n-form-item label="邮箱待验证提醒">
<n-switch v-model:value="settings.enabled_types.verify_email" />
@@ -90,8 +96,10 @@ const settings = reactive({
comment_reply: true,
comment_approved: true,
download_ready: true,
credits_added: true,
credits_deducted: true,
asset_points_increase: true,
asset_points_decrease: true,
asset_balance_increase: true,
asset_balance_decrease: true,
verify_email: true,
pending_comments: true,
upload_moderation: true,
@@ -55,10 +55,10 @@
<n-select v-model:value="createForm.type" :options="typeOptions" />
</n-form-item>
<n-form-item label="卡密">
<n-input v-model:value="createForm.code" placeholder="手动填写唯一卡密" />
<n-input v-model:value="createForm.password" 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-input v-model:value="createForm.amount" inputmode="decimal" placeholder="例如 10.00" />
</n-form-item>
<n-grid v-if="createForm.type === 'vip'" cols="2" :x-gap="12">
<n-grid-item>
@@ -101,7 +101,7 @@
</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-input v-model:value="batchForm.amount" inputmode="decimal" placeholder="例如 10.00" />
</n-form-item>
</n-grid-item>
<n-grid-item v-if="batchForm.type === 'credits'">
@@ -185,8 +185,8 @@ const filters = reactive({
const createForm = reactive({
type: 'balance',
code: '',
amount: 10,
password: '',
amount: '10.00',
level: 1,
days: 30,
points: 100,
@@ -197,7 +197,7 @@ const createForm = reactive({
const batchForm = reactive({
type: 'balance',
count: 20,
amount: 10,
amount: '10.00',
level: 1,
days: 30,
points: 100,
@@ -265,7 +265,7 @@ const pagination = computed(() => ({
const columns = [
{ title: 'ID', key: 'id', width: 70 },
{ title: '卡密', key: 'code', minWidth: 170, ellipsis: { tooltip: true } },
{ title: '卡密', key: 'password', minWidth: 170, ellipsis: { tooltip: true } },
{
title: '类型',
key: 'type_label',
@@ -408,9 +408,8 @@ async function handleCreate() {
method: 'POST',
data: {
type: createForm.type,
code: createForm.code,
password: createForm.code,
amount: createForm.amount,
password: createForm.password,
amount: String(createForm.amount ?? ''),
level: createForm.level,
days: createForm.days,
points: createForm.points,
@@ -420,7 +419,7 @@ async function handleCreate() {
})
message.success('充值卡已创建')
createVisible.value = false
createForm.code = ''
createForm.password = ''
createForm.note = ''
await loadData()
} catch (error) {
@@ -438,7 +437,7 @@ async function handleBatchCreate() {
data: {
type: batchForm.type,
count: batchForm.count,
amount: batchForm.amount,
amount: String(batchForm.amount ?? ''),
level: batchForm.level,
days: batchForm.days,
points: batchForm.points,
@@ -461,7 +460,7 @@ async function handleBatchCreate() {
function handleDisable(row) {
dialog.warning({
title: '确认禁用',
content: `确定要禁用充值卡 ${row.code} 吗?`,
content: `确定要禁用充值卡 ${row.password} 吗?`,
positiveText: '禁用',
negativeText: '取消',
onPositiveClick: async () => {
@@ -488,7 +487,7 @@ async function handleExport() {
const header = ['ID', '卡号', '面额', '余额', '状态', '使用人', '创建时间', '过期时间', '备注']
const lines = items.map((row) => [
row.id,
row.code,
row.password,
row.amount,
row.balance,
row.status_label,
@@ -1,315 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="返佣管理" desc="按状态、类型和时间筛选返佣记录,自定义列展示并在详情中查看处理历史。" />
<div class="aksrt-rebate-page__stats">
<n-card v-for="card in statCards" :key="card.key" size="small">
<n-statistic :label="card.label" :value="stats[card.key] || 0" />
</n-card>
</div>
<n-card size="small" title="筛选与列控制">
<n-space vertical :size="12">
<n-grid cols="1 s:2 m:5" :x-gap="12" :y-gap="12" responsive="screen">
<n-grid-item>
<n-input v-model:value="filters.keyword" clearable placeholder="搜索订单号、用户、备注" />
</n-grid-item>
<n-grid-item>
<n-select v-model:value="filters.status" :options="statusOptions" placeholder="状态" />
</n-grid-item>
<n-grid-item>
<n-select v-model:value="filters.type" :options="typeOptions" placeholder="类型" />
</n-grid-item>
<n-grid-item>
<n-date-picker v-model:value="filters.dateRange" type="daterange" clearable style="width: 100%;" />
</n-grid-item>
<n-grid-item>
<n-space>
<n-button type="primary" @click="handleSearch">筛选</n-button>
<n-button @click="resetFilters">重置</n-button>
</n-space>
</n-grid-item>
</n-grid>
<n-checkbox-group v-model:value="visibleColumnKeys">
<n-space wrap>
<n-checkbox v-for="column in columnChoices" :key="column.value" :value="column.value">
{{ column.label }}
</n-checkbox>
</n-space>
</n-checkbox-group>
</n-space>
</n-card>
<n-card class="aksrt-rebate-page__card" size="small" title="返佣台账">
<n-data-table
:columns="tableColumns"
:data="rows"
:loading="loading"
:bordered="false"
:pagination="pagination"
:row-key="(row) => row.id"
/>
</n-card>
<RebateDetailDrawer
v-model:show="drawerVisible"
:rebate-id="currentRebateId"
@updated="loadData"
/>
</div>
</template>
<script setup>
import { computed, h, onMounted, reactive, ref } from 'vue'
import { NButton, NSpace, NTag, useMessage } from 'naive-ui'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import RebateDetailDrawer from './components/RebateDetailDrawer.vue'
const message = useMessage()
const loading = ref(false)
const rows = ref([])
const drawerVisible = ref(false)
const currentRebateId = ref(null)
const stats = reactive({
total: 0,
pending: 0,
settled: 0,
total_amount: 0,
settled_amount: 0,
})
const filters = reactive({
keyword: '',
status: '',
type: '',
dateRange: null,
})
const pager = reactive({
page: 1,
pageSize: 20,
itemCount: 0,
})
const optionsState = reactive({
statuses: [],
types: [],
})
const statCards = [
{ key: 'total', label: '总记录' },
{ key: 'pending', label: '待结算' },
{ key: 'settled', label: '已结算' },
{ key: 'total_amount', label: '总返佣' },
{ key: 'settled_amount', label: '已结算金额' },
]
const statusOptions = computed(() => optionsState.statuses.length
? optionsState.statuses
: [
{ label: '全部状态', value: '' },
{ label: '待结算', value: 'pending' },
{ label: '已结算', value: 'settled' },
{ label: '已取消', value: 'cancelled' },
])
const typeOptions = computed(() => optionsState.types.length
? optionsState.types
: [
{ label: '全部类型', value: '' },
{ label: '订单返佣', value: 'order' },
])
const columnChoices = [
{ label: '来源用户', value: 'source_user' },
{ label: '创建时间', value: 'created_at' },
{ label: '结算时间', value: 'settled_at' },
{ label: '备注', value: 'note' },
{ label: '处理记录数', value: 'history_count' },
]
const visibleColumnKeys = ref(['source_user', 'created_at', 'settled_at', 'history_count'])
const baseColumns = computed(() => {
const columns = [
{ title: 'ID', key: 'id', width: 70 },
{ title: '订单号', key: 'order_number', minWidth: 180, ellipsis: { tooltip: true } },
{
title: '返佣对象',
key: 'user',
width: 160,
render: (row) => row.user?.display_name || '-',
},
{
title: '返佣金额',
key: 'amount',
width: 100,
},
{
title: '状态',
key: 'status',
width: 100,
render: (row) => {
const typeMap = { pending: 'warning', settled: 'success', cancelled: 'default' }
return h(NTag, { size: 'small', round: true, type: typeMap[row.status] || 'default' }, () => row.status_label)
},
},
{
title: '类型',
key: 'type',
width: 100,
render: (row) => row.type_label || row.type,
},
]
if (visibleColumnKeys.value.includes('source_user')) {
columns.push({
title: '来源用户',
key: 'source_user',
width: 160,
render: (row) => row.source_user?.display_name || '-',
})
}
if (visibleColumnKeys.value.includes('created_at')) {
columns.push({ title: '创建时间', key: 'created_at', width: 170 })
}
if (visibleColumnKeys.value.includes('settled_at')) {
columns.push({
title: '结算时间',
key: 'settled_at',
width: 170,
render: (row) => row.settled_at || '-',
})
}
if (visibleColumnKeys.value.includes('note')) {
columns.push({
title: '备注',
key: 'note',
minWidth: 160,
ellipsis: { tooltip: true },
render: (row) => row.note || '-',
})
}
if (visibleColumnKeys.value.includes('history_count')) {
columns.push({ title: '处理记录数', key: 'history_count', width: 100 })
}
columns.push({
title: '操作',
key: 'actions',
width: 120,
render: (row) => h(NSpace, { size: 'small' }, () => [
h(NButton, {
size: 'tiny',
type: 'primary',
quaternary: true,
onClick: () => openDrawer(row.id),
}, () => '详情'),
]),
})
return columns
})
const tableColumns = computed(() => baseColumns.value)
const pagination = computed(() => ({
page: pager.page,
pageSize: pager.pageSize,
itemCount: pager.itemCount,
showSizePicker: true,
pageSizes: [10, 20, 50, 100],
onUpdatePage: (page) => {
pager.page = page
loadData()
},
onUpdatePageSize: (pageSize) => {
pager.pageSize = pageSize
pager.page = 1
loadData()
},
}))
onMounted(() => {
loadData()
})
function formatDateValue(value) {
const date = new Date(value)
const year = date.getFullYear()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
return `${year}-${month}-${day}`
}
function buildParams() {
const params = {
page: pager.page,
per_page: pager.pageSize,
}
if (filters.keyword) params.keyword = filters.keyword
if (filters.status) params.status = filters.status
if (filters.type) params.type = filters.type
if (Array.isArray(filters.dateRange) && filters.dateRange.length === 2) {
params.date_from = formatDateValue(filters.dateRange[0])
params.date_to = formatDateValue(filters.dateRange[1])
}
return params
}
async function loadData() {
loading.value = true
try {
const res = await api.request('/payment/rebate/log', { params: buildParams() })
const data = res?.data || {}
rows.value = data.items || []
pager.itemCount = data.total || 0
Object.assign(stats, data.stats || {})
optionsState.statuses = data.options?.statuses || []
optionsState.types = data.options?.types || []
} catch (error) {
message.error(error?.message || '加载返佣台账失败')
} finally {
loading.value = false
}
}
function handleSearch() {
pager.page = 1
loadData()
}
function resetFilters() {
filters.keyword = ''
filters.status = ''
filters.type = ''
filters.dateRange = null
pager.page = 1
loadData()
}
function openDrawer(id) {
currentRebateId.value = id
drawerVisible.value = true
}
</script>
<style scoped>
.aksrt-rebate-page__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.aksrt-rebate-page__card {
margin-top: 16px;
}
</style>
@@ -49,7 +49,7 @@
</div>
<n-empty v-else size="small" description="暂无数据" />
<div class="aksrt-revenue__chart-summary">
<span>期间总计: <strong>{{ formatAmount(group.total, group.key) }}</strong></span>
<span>期间总计: <strong>{{ formatAmount(group.total, group.key) }}</strong></span>
<span>共 {{ group.count }} 笔</span>
</div>
</div>
@@ -91,7 +91,7 @@
</n-grid-item>
<n-grid-item>
<div class="aksrt-revenue__stat">
<strong>{{ withdrawalStats.total_paid.toFixed(2) }}</strong>
<strong>{{ formatAmount(withdrawalStats.total_paid, 'cash') }}</strong>
<p>已提现 (元)</p>
</div>
</n-grid-item>
@@ -106,16 +106,21 @@ import { ref, computed, onMounted } from 'vue'
import PaymentSectionPage from '@/pages/payment/PaymentSectionPage.vue'
import api from '@/api'
const emptyCurrencyStats = () => ({ total: 0, pending: 0, settled: 0, count: 0 })
const revenueStats = ref({ cash: emptyCurrencyStats(), points: emptyCurrencyStats() })
const withdrawalStats = ref({ total: 0, pending: 0, processing: 0, completed: 0, rejected: 0, total_paid: 0 })
const emptyCurrencyStats = (currency) => ({
total: currency === 'cash' ? '0.00' : 0,
pending: currency === 'cash' ? '0.00' : 0,
settled: currency === 'cash' ? '0.00' : 0,
count: 0,
})
const revenueStats = ref({ cash: emptyCurrencyStats('cash'), points: emptyCurrencyStats('points') })
const withdrawalStats = ref({ total: 0, pending: 0, processing: 0, completed: 0, rejected: 0, total_paid: '0.00' })
const timeSeries = ref({ cash: [], points: [] })
const granularity = ref('day')
const revenueSummary = computed(() => [
{ key: 'cash-total', value: `${revenueStats.value.cash.total.toFixed(2)} 元`, label: '现金总收益' },
{ key: 'cash-pending', value: `${revenueStats.value.cash.pending.toFixed(2)} 元`, label: '现金待结算' },
{ key: 'cash-settled', value: `${revenueStats.value.cash.settled.toFixed(2)} 元`, label: '现金已结算' },
{ key: 'cash-total', value: `${formatAmount(revenueStats.value.cash.total, 'cash')} 元`, label: '现金总收益' },
{ key: 'cash-pending', value: `${formatAmount(revenueStats.value.cash.pending, 'cash')} 元`, label: '现金待结算' },
{ key: 'cash-settled', value: `${formatAmount(revenueStats.value.cash.settled, 'cash')} 元`, label: '现金已结算' },
{ key: 'cash-count', value: revenueStats.value.cash.count, label: '现金收益笔数' },
{ key: 'points-total', value: `${formatAmount(revenueStats.value.points.total, 'points')}`, label: '积分总收益' },
{ key: 'points-pending', value: `${formatAmount(revenueStats.value.points.pending, 'points')}`, label: '积分待结算' },
@@ -134,21 +139,44 @@ const timeSeriesGroups = computed(() => [
label: '积分收益',
items: Array.isArray(timeSeries.value.points) ? timeSeries.value.points : [],
},
].map((group) => ({
...group,
total: group.items.reduce((sum, item) => sum + Number(item.amount || 0), 0),
count: group.items.reduce((sum, item) => sum + Number(item.count || 0), 0),
})))
].map((group) => {
const totalMinor = group.items.reduce((sum, item) => sum + amountMinor(item.amount, group.key), 0n)
return {
...group,
total: formatMinor(totalMinor, group.key),
count: group.items.reduce((sum, item) => sum + Number(item.count || 0), 0),
}
}))
const hasTimeSeries = computed(() => timeSeriesGroups.value.some((group) => group.items.length > 0))
function formatAmount(amount, currency) {
return currency === 'points' ? `${Number(amount || 0).toFixed(0)} 积分` : `¥${Number(amount || 0).toFixed(2)}`
return currency === 'points'
? `${formatMinor(amountMinor(amount, currency), currency)} 积分`
: `¥${formatMinor(amountMinor(amount, currency), currency)}`
}
function amountMinor(amount, currency) {
const raw = String(amount ?? '').trim()
if (currency === 'points') return /^\d+$/.test(raw) ? BigInt(raw) : 0n
const match = /^(0|[1-9]\d*)(?:\.(\d{1,2}))?$/.exec(raw)
return match ? (BigInt(match[1]) * 100n) + BigInt((match[2] || '').padEnd(2, '0') || '0') : 0n
}
function formatMinor(minor, currency) {
if (currency === 'points') return String(minor)
return `${minor / 100n}.${String(minor % 100n).padStart(2, '0')}`
}
function getBarHeight(amount, items) {
const maxAmount = Math.max(1, ...items.map((item) => Number(item.amount || 0)))
return Math.max(2, (Number(amount || 0) / maxAmount) * 100)
const currency = items === timeSeries.value.points ? 'points' : 'cash'
const maxAmount = items.reduce((max, item) => {
const current = amountMinor(item.amount, currency)
return current > max ? current : max
}, 0n)
if (maxAmount <= 0n) return 2
const scaled = Number((amountMinor(amount, currency) * 10000n) / maxAmount) / 100
return Math.max(2, scaled)
}
function formatLabel(date) {
@@ -170,8 +198,8 @@ async function loadRevenue() {
if (res?.data) {
if (res.data.revenue) {
revenueStats.value = {
cash: { ...emptyCurrencyStats(), ...(res.data.revenue.cash || {}) },
points: { ...emptyCurrencyStats(), ...(res.data.revenue.points || {}) },
cash: { ...emptyCurrencyStats('cash'), ...(res.data.revenue.cash || {}) },
points: { ...emptyCurrencyStats('points'), ...(res.data.revenue.points || {}) },
}
}
if (res.data.withdrawal) {
@@ -181,7 +209,7 @@ async function loadRevenue() {
processing: Number(res.data.withdrawal.processing || 0),
completed: Number(res.data.withdrawal.completed || 0),
rejected: Number(res.data.withdrawal.rejected || 0),
total_paid: Number(res.data.withdrawal.total_paid || 0),
total_paid: String(res.data.withdrawal.total_paid || '0.00'),
}
}
timeSeries.value = {
@@ -4,7 +4,7 @@
<n-spin :show="loading">
<template v-if="detail.card">
<n-descriptions bordered :column="1" size="small" label-placement="left">
<n-descriptions-item label="卡号">{{ detail.card.code || '-' }}</n-descriptions-item>
<n-descriptions-item label="卡密">{{ detail.card.password || '-' }}</n-descriptions-item>
<n-descriptions-item label="面额">{{ detail.card.amount || 0 }}</n-descriptions-item>
<n-descriptions-item label="余额">{{ detail.card.balance || 0 }}</n-descriptions-item>
<n-descriptions-item label="状态">{{ detail.card.status_label || detail.card.status }}</n-descriptions-item>
@@ -1,175 +0,0 @@
<template>
<n-drawer :show="show" :width="720" @update:show="$emit('update:show', $event)">
<n-drawer-content title="返佣详情" closable>
<n-spin :show="loading">
<template v-if="detail.record">
<n-descriptions bordered :column="1" size="small" label-placement="left">
<n-descriptions-item label="订单号">{{ detail.record.order_number || '-' }}</n-descriptions-item>
<n-descriptions-item label="返佣对象">{{ detail.record.user?.display_name || '-' }}</n-descriptions-item>
<n-descriptions-item label="来源用户">{{ detail.record.source_user?.display_name || '-' }}</n-descriptions-item>
<n-descriptions-item label="返佣金额">{{ detail.record.amount || 0 }}</n-descriptions-item>
<n-descriptions-item label="类型">{{ detail.record.type_label || detail.record.type }}</n-descriptions-item>
<n-descriptions-item label="状态">{{ detail.record.status_label || detail.record.status }}</n-descriptions-item>
<n-descriptions-item label="创建时间">{{ detail.record.created_at || '-' }}</n-descriptions-item>
<n-descriptions-item label="结算时间">{{ detail.record.settled_at || '-' }}</n-descriptions-item>
</n-descriptions>
<div class="aksrt-rebate-drawer__section">
<div class="aksrt-rebate-drawer__title">备注</div>
<n-alert type="info" :show-icon="false">
{{ detail.record.note || '暂无备注' }}
</n-alert>
</div>
<div class="aksrt-rebate-drawer__section">
<div class="aksrt-rebate-drawer__title">处理记录</div>
<div v-if="detail.history?.length" class="aksrt-rebate-drawer__timeline">
<div v-for="(entry, index) in detail.history" :key="index" class="aksrt-rebate-drawer__timeline-item">
<div class="aksrt-rebate-drawer__timeline-head">
<strong>{{ entry.event || '-' }}</strong>
<span>{{ entry.time || '-' }}</span>
</div>
<p>{{ entry.note || '无备注' }}</p>
<small>{{ entry.operator_name || '系统' }}</small>
</div>
</div>
<n-empty v-else description="暂无处理记录" />
</div>
<div v-if="detail.record.status === 'pending'" class="aksrt-rebate-drawer__section">
<div class="aksrt-rebate-drawer__title">处理返佣</div>
<n-space vertical :size="12">
<n-input
v-model:value="form.note"
type="textarea"
:rows="3"
placeholder="记录结算说明或取消原因"
/>
<n-space>
<n-button type="primary" :loading="submitting === 'settle'" @click="handleAction('settle')">
结算返佣
</n-button>
<n-button type="warning" secondary :loading="submitting === 'cancel'" @click="handleAction('cancel')">
取消返佣
</n-button>
</n-space>
</n-space>
</div>
</template>
<n-empty v-else description="暂无返佣详情" />
</n-spin>
</n-drawer-content>
</n-drawer>
</template>
<script setup>
import { reactive, ref, watch } from 'vue'
import { useMessage } from 'naive-ui'
import api from '@/api'
const props = defineProps({
show: { type: Boolean, default: false },
rebateId: { type: [Number, String], default: null },
})
const emit = defineEmits(['update:show', 'updated'])
const message = useMessage()
const loading = ref(false)
const submitting = ref('')
const detail = ref({ record: null, history: [] })
const form = reactive({ note: '' })
watch(
() => [props.show, props.rebateId],
async ([visible, id]) => {
if (visible && id) {
await loadDetail()
}
if (!visible) {
detail.value = { record: null, history: [] }
form.note = ''
}
},
)
async function loadDetail() {
if (!props.rebateId) {
return
}
loading.value = true
try {
const res = await api.request(`/payment/rebate/${props.rebateId}`)
detail.value = res?.data || { record: null, history: [] }
form.note = ''
} catch (error) {
message.error(error?.message || '加载返佣详情失败')
} finally {
loading.value = false
}
}
async function handleAction(action) {
if (!props.rebateId) {
return
}
submitting.value = action
try {
await api.request(`/payment/rebate/${props.rebateId}/${action}`, {
method: 'POST',
data: { note: form.note },
})
message.success(action === 'settle' ? '返佣已结算' : '返佣已取消')
await loadDetail()
emit('updated')
} catch (error) {
message.error(error?.message || '处理失败')
} finally {
submitting.value = ''
}
}
</script>
<style scoped>
.aksrt-rebate-drawer__section {
margin-top: 20px;
}
.aksrt-rebate-drawer__title {
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
.aksrt-rebate-drawer__timeline {
display: grid;
gap: 12px;
}
.aksrt-rebate-drawer__timeline-item {
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #f8fafc;
}
.aksrt-rebate-drawer__timeline-head {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
}
.aksrt-rebate-drawer__timeline-item p {
margin: 0 0 6px;
color: #111827;
white-space: pre-wrap;
}
.aksrt-rebate-drawer__timeline-item small {
color: #6b7280;
}
</style>
@@ -163,8 +163,10 @@ function getTypeTag(type) {
system: 'info',
comment_reply: 'success',
comment_approved: 'success',
credits_added: 'warning',
credits_deducted: 'warning',
asset_points_increase: 'warning',
asset_points_decrease: 'warning',
asset_balance_increase: 'success',
asset_balance_decrease: 'error',
download_ready: 'primary',
verify_email: 'info',
pending_comments: 'warning',
@@ -178,8 +180,10 @@ function getTypeLabel(type) {
system: '系统通知',
comment_reply: '评论回复',
comment_approved: '评论审核',
credits_added: '积分增加',
credits_deducted: '积分扣减',
asset_points_increase: '积分增加',
asset_points_decrease: '积分扣减',
asset_balance_increase: '余额增加',
asset_balance_decrease: '余额扣减',
download_ready: '下载就绪',
verify_email: '邮箱验证',
pending_comments: '待审评论',
+4 -4
View File
@@ -37,8 +37,8 @@
<n-form-item label="云失败转人工"><n-switch v-model:value="verifyForm.user_verify_allow_manual_fallback" /></n-form-item>
<n-form-item label="认证会话有效期(分钟)"><n-input-number v-model:value="verifyForm.user_verify_session_ttl_minutes" :min="5" :max="1440" /></n-form-item>
<n-form-item label="结果轮询间隔(秒)"><n-input-number v-model:value="verifyForm.user_verify_poll_interval_seconds" :min="3" :max="60" /></n-form-item>
<n-form-item label="个人认证费用"><n-input-number v-model:value="verifyForm.user_verify_personal_fee" :min="0" :step="0.01" /></n-form-item>
<n-form-item label="企业认证费用"><n-input-number v-model:value="verifyForm.user_verify_enterprise_fee" :min="0" :step="0.01" /></n-form-item>
<n-form-item label="个人认证费用"><n-input v-model:value="verifyForm.user_verify_personal_fee" placeholder="0.00" /></n-form-item>
<n-form-item label="企业认证费用"><n-input v-model:value="verifyForm.user_verify_enterprise_fee" placeholder="0.00" /></n-form-item>
<n-form-item label="个人认证徽标文案">
<n-input v-model:value="verifyForm.user_verify_personal_badge_text" placeholder="个人认证" />
</n-form-item>
@@ -174,8 +174,8 @@ const verifyDefaults = {
user_verify_allow_manual_fallback: true,
user_verify_session_ttl_minutes: 30,
user_verify_poll_interval_seconds: 5,
user_verify_personal_fee: 0,
user_verify_enterprise_fee: 0,
user_verify_personal_fee: '0.00',
user_verify_enterprise_fee: '0.00',
user_verify_personal_badge_text: '个人认证',
user_verify_enterprise_badge_text: '企业认证',
user_verify_disable_desc: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
-1
View File
@@ -78,7 +78,6 @@ function createSectionRedirectRoutes() {
const NATIVE_OPS_REDIRECTS = [
{ path: '/payment/charge-card', title: '充值卡', nativeAdminPath: 'admin.php?page=aksrt_charge_cards' },
{ path: '/payment/income', title: '创作分成运营', nativeAdminPath: 'admin.php?page=aksrt_income' },
{ path: '/payment/rebate', title: '返佣管理', nativeAdminPath: 'admin.php?page=aksrt_rebate' },
{ path: '/users/messages', title: '消息中心', nativeAdminPath: 'users.php?page=aksrt_messages' },
{ path: '/users/referral-codes', title: '推广码运营', nativeAdminPath: 'users.php?page=aksrt_referral_codes' },
{ path: '/users/ban', title: '禁封管理', nativeAdminPath: 'users.php?page=aksrt_user_ban&tab=restrictions' },
@@ -12,7 +12,7 @@ export const cardPassSchema = {
card_pass_batch_size: 50,
card_pass_max_redeem: 1,
card_pass_expiry: 0,
card_pass_amount: 10,
card_pass_amount: '10.00',
card_pass_vip_days: 30,
card_pass_credits: 0,
card_pass_redeem_page: '',
@@ -40,7 +40,7 @@ export const cardPassSchema = {
fields: [
{ 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: 'number', default: 10, min: 1, max: 9999, dependsOn: { key: 'card_pass_enabled', value: true } },
{ key: 'card_pass_amount', label: '面额(元)', type: 'input', 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 } },
],
@@ -6,7 +6,6 @@ export const rechargeSchema = {
defaults: {
pay_balance_s: true,
pay_balance_desc: '',
pay_auto_to_balance: true,
pay_balance_product: '30|30|入门包\n98|108|推荐包',
pay_balance_product_name: '',
pay_balance_price: 0,
@@ -34,7 +33,6 @@ export const rechargeSchema = {
fields: [
{ key: 'pay_balance_s', label: '启用余额功能', type: 'switch', default: true, help: '开启后用户中心显示现金钱包,并默认允许余额支付(可在收款接口单独关闭)。充值需另配支付宝/微信等外部网关。' },
{ key: 'pay_balance_desc', label: '充值说明', type: 'textarea', default: '', dependsOn: { key: 'pay_balance_s', value: true } },
{ key: 'pay_auto_to_balance', label: '自动到账到余额', type: 'switch', default: true, dependsOn: { key: 'pay_balance_s', value: true }, help: '充值订单支付成功后自动入账钱包(卡密/管理员调整不受此开关限制)。' },
],
},
{
@@ -35,7 +35,7 @@ export const vipSchema = {
vip_renew_desc: '',
vip_renew_price_type: 'discount',
vip_renew_discount: 8,
vip_renew_reduce: 20,
vip_renew_reduce: '20.00',
vip_upgrade: true,
vip_upgrade_desc: '',
vip_upgrade_product: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
@@ -126,7 +126,7 @@ export const vipSchema = {
{ key: 'vip_renew_desc', label: '续费说明', type: 'textarea', default: '', dependsOn: { key: 'vip_renew', value: true } },
{ key: 'vip_renew_price_type', label: '续费价格类型', type: 'radio', default: 'discount', options: [{ label: '相同价格', value: 'same' }, { label: '折扣价格', value: 'discount' }, { label: '优惠立减', value: 'reduce' }], dependsOn: { key: 'vip_renew', value: true } },
{ key: 'vip_renew_discount', label: '续费折扣(折,如 8=八折)', type: 'number', default: 8, min: 1, max: 100, dependsOn: { key: 'vip_renew_price_type', value: 'discount' } },
{ key: 'vip_renew_reduce', label: '续费减免金额', type: 'number', default: 20, min: 0, max: 9999, dependsOn: { key: 'vip_renew_price_type', value: 'reduce' } },
{ key: 'vip_renew_reduce', label: '续费减免金额', type: 'input', sanitize_type: 'money_decimal', default: '20.00', min: 0, max: 9999, dependsOn: { key: 'vip_renew_price_type', value: 'reduce' } },
],
},
{
@@ -4,10 +4,10 @@ export const withdrawalSchema = {
icon: 'fa fas fa-money-bill',
description: '配置提现门槛、手续费比例与每日限制。',
defaults: {
affiliate_min_withdrawal: 50,
affiliate_min_withdrawal: '50.00',
affiliate_withdrawal_fee: 5,
affiliate_daily_withdrawal_count: 3,
affiliate_daily_withdrawal_limit: 500,
affiliate_daily_withdrawal_limit: '500.00',
affiliate_withdrawal_reset_time: '00:00',
affiliate_payout_channels: ['alipay', 'wechat'],
},
@@ -17,10 +17,10 @@ export const withdrawalSchema = {
title: '提现设置',
icon: 'fa fas fa-sliders-h',
fields: [
{ key: 'affiliate_min_withdrawal', label: '最低提现金额', type: 'number', default: 50, min: 0, max: 99999 },
{ key: 'affiliate_min_withdrawal', label: '最低提现金额', type: 'input', sanitize_type: 'money_decimal', default: '50.00', help: '仅接受最多两位小数的十进制字符串。' },
{ key: 'affiliate_withdrawal_fee', label: '提现手续费(%)', type: 'number', default: 5, min: 0, max: 100 },
{ key: 'affiliate_daily_withdrawal_count', label: '每日可提现次数', type: 'number', default: 3, min: 0, max: 99999 },
{ key: 'affiliate_daily_withdrawal_limit', label: '每日提现上限', type: 'number', default: 500, min: 0, max: 999999 },
{ key: 'affiliate_daily_withdrawal_limit', label: '每日提现上限', type: 'input', sanitize_type: 'money_decimal', default: '500.00', help: '仅接受最多两位小数的十进制字符串。' },
{ key: 'affiliate_withdrawal_reset_time', label: '重置时间', type: 'input', default: '00:00', help: '按站点 WordPress 时区重置,格式 HH:MM。' },
{ key: 'affiliate_payout_channels', label: '可用收款渠道', type: 'checkbox', default: ['alipay', 'wechat'], sanitize_type: 'array', options: [{ label: '支付宝收款码', value: 'alipay' }, { label: '微信收款码', value: 'wechat' }] },
],
@@ -141,8 +141,8 @@ export const pageSchema = {
{ key: 'post_rewards_text', label: '打赏按钮文案', type: 'input', default: '赞赏', dependsOn: { key: 'post_rewards_s', value: true } },
{ key: 'post_rewards_amounts', label: '快捷金额', type: 'input', default: '5,10,20,50,100', help: '使用英文逗号分隔', dependsOn: { key: 'post_rewards_s', value: true } },
{ key: 'post_rewards_custom', label: '允许自定义金额', type: 'switch', default: true, dependsOn: { key: 'post_rewards_s', value: true } },
{ key: 'post_rewards_min', label: '最低金额', type: 'number', default: 1, min: 0.01, max: 1000, step: 0.01, dependsOn: { key: 'post_rewards_s', value: true } },
{ key: 'post_rewards_max', label: '最高金额', type: 'number', default: 1000, min: 1, max: 100000, step: 0.01, dependsOn: { key: 'post_rewards_s', value: true } },
{ key: 'post_rewards_min', label: '最低金额', type: 'input', default: '1.00', min: 0.01, max: 1000, step: 0.01, dependsOn: { key: 'post_rewards_s', value: true } },
{ key: 'post_rewards_max', label: '最高金额', type: 'input', default: '1000.00', min: 1, max: 100000, step: 0.01, dependsOn: { key: 'post_rewards_s', value: true } },
{ key: 'article_maxheight_kg', label: '正文限高', type: 'switch', default: false, help: '开启后长文默认折叠,可展开全文' },
{ key: 'article_maxheight', label: '限高像素', type: 'number', default: 1000, min: 200, max: 5000, dependsOn: { key: 'article_maxheight_kg', value: true } },
],
@@ -27,8 +27,8 @@ export const submitSchema = {
post_article_title_strlen_limit_max: 30,
post_article_user: 0,
post_article_pay_free_s: true,
post_article_pay_price_min: 0,
post_article_pay_price_max: 100,
post_article_pay_price_min: '0.00',
post_article_pay_price_max: '100.00',
post_article_pay_vip_price_s: true,
post_article_pay_vip_1_discount: 80,
post_article_pay_vip_2_discount: 50,
@@ -114,8 +114,8 @@ export const submitSchema = {
icon: 'fa fas fa-database',
fields: [
{ key: 'post_article_pay_free_s', label: '允许设置免费', type: 'switch', default: true, dependsOn: { key: 'front_submit_enabled', value: true } },
{ key: 'post_article_pay_price_min', label: '最低价格', type: 'number', default: 0, min: 0, max: 9999, help: '投稿可设置的最低付费金额', dependsOn: { key: 'front_submit_enabled', value: true } },
{ key: 'post_article_pay_price_max', label: '最高价格', type: 'number', default: 100, min: 0, max: 9999, help: '投稿可设置的最高付费金额', dependsOn: { key: 'front_submit_enabled', value: true } },
{ key: 'post_article_pay_price_min', label: '最低价格', type: 'input', sanitize_type: 'money_decimal', default: '0.00', min: 0, max: 9999, help: '投稿可设置的最低付费金额', dependsOn: { key: 'front_submit_enabled', value: true } },
{ key: 'post_article_pay_price_max', label: '最高价格', type: 'input', sanitize_type: 'money_decimal', default: '100.00', min: 0, max: 9999, help: '投稿可设置的最高付费金额', dependsOn: { key: 'front_submit_enabled', value: true } },
{ key: 'post_article_pay_vip_price_s', label: '允许设置会员价', type: 'switch', default: true, dependsOn: { key: 'front_submit_enabled', value: true } },
{ key: 'post_article_pay_vip_1_discount', label: 'VIP1 折扣(%)', type: 'number', default: 80, min: 0, max: 100, help: 'VIP1 会员折扣百分比,80 即 8 折', dependsOn: { key: 'front_submit_enabled', value: true } },
{ key: 'post_article_pay_vip_2_discount', label: 'VIP2 折扣(%)', type: 'number', default: 50, min: 0, max: 100, help: 'VIP2 会员折扣百分比,50 即 5 折', dependsOn: { key: 'front_submit_enabled', value: true } },
@@ -12,8 +12,8 @@ export const MEDAL_GET_TYPES = [
{ label: '文章获赞', value: 'like' },
{ label: '评论获赞', value: 'comment_like' },
{ label: '内容阅读量', value: 'views' },
{ label: '现金创作分成累计(元)', value: 'pay_income' },
{ label: '推广佣金累计(元)', value: 'pay_rebate' },
{ label: '现金创作分成累计(元)', value: 'creator_income' },
{ label: '推广佣金累计(元)', value: 'affiliate_commission' },
{ label: '仅手动授予', value: 'manually_add' },
]
@@ -26,9 +26,9 @@ export const creditsSchema = {
points_deduction_s: false,
points_deduction_rate: 100,
points_deduction_max: 50,
points_pass_exchange_s: false,
points_pass_exchange_desc: '',
points_pass_exchange_only_password: true,
asset_points_card_enabled: false,
asset_points_card_description: '',
asset_points_card_password_only: true,
points_show_balance: true,
points_show_log: true,
points_expire_days: 0,
@@ -95,9 +95,9 @@ export const creditsSchema = {
title: '积分卡密兑换',
icon: 'fa fas fa-ticket-alt',
fields: [
{ key: 'points_pass_exchange_s', label: '启用积分卡密兑换', type: 'switch', default: false, help: '对齐 legacy points_pass_exchange_s。后台充值卡可生成积分卡,用户中心兑换入账。' },
{ key: 'points_pass_exchange_desc', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'points_pass_exchange_s', value: true } },
{ key: 'points_pass_exchange_only_password', label: '仅密码兑换', type: 'switch', default: true, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '开启后只需输入卡密;关闭则需卡号+卡密。' },
{ key: 'asset_points_card_enabled', label: '启用积分卡密兑换', type: 'switch', default: false, help: '后台充值卡可生成积分卡,用户中心兑换入账。' },
{ key: 'asset_points_card_description', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'asset_points_card_enabled', value: true } },
{ key: 'asset_points_card_password_only', label: '仅密码兑换', type: 'switch', default: true, dependsOn: { key: 'asset_points_card_enabled', value: true }, help: '开启后只需输入卡密;关闭则需卡号+卡密。' },
],
},
{
@@ -7,11 +7,11 @@ export const verifySchema = {
user_verify_enabled: true,
user_verify_public_display: true,
user_verify_personal_badge_text: '个人认证',
user_verify_personal_fee: 0,
user_verify_personal_fee: '0.00',
user_verify_personal_free_applications: 0,
user_verify_personal_require_real_name: false,
user_verify_enterprise_badge_text: '企业认证',
user_verify_enterprise_fee: 0,
user_verify_enterprise_fee: '0.00',
user_verify_enterprise_free_applications: 0,
user_verify_enterprise_require_real_name: false,
user_verify_allow_apply: true,
@@ -67,10 +67,10 @@ export const verifySchema = {
default: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
dependsOn: { key: 'user_verify_allow_apply', value: false },
},
{ key: 'user_verify_personal_fee', label: '个人认证每次扣余额(元)', type: 'number', default: 0, min: 0, step: 0.01, dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_personal_fee', label: '个人认证每次扣余额(元)', type: 'input', default: '0.00', help: '仅接受最多两位小数的十进制字符串。', dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_personal_free_applications', label: '个人认证赠送初始次数', type: 'number', default: 0, min: 0, max: 9999, help: '每位用户前 N 次个人认证申请免费,之后按单次费用扣余额。', dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_personal_require_real_name', label: '个人认证强制填写真实姓名', type: 'switch', default: false, dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_enterprise_fee', label: '企业认证每次扣余额(元)', type: 'number', default: 0, min: 0, step: 0.01, dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_enterprise_fee', label: '企业认证每次扣余额(元)', type: 'input', default: '0.00', help: '仅接受最多两位小数的十进制字符串。', dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_enterprise_free_applications', label: '企业认证赠送初始次数', type: 'number', default: 0, min: 0, max: 9999, help: '每位用户前 N 次企业认证申请免费,之后按单次费用扣余额。', dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_enterprise_require_real_name', label: '企业认证强制填写真实姓名', type: 'switch', default: false, dependsOn: { key: 'user_verify_allow_apply', value: true } },
{