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

This commit is contained in:
kate522 committed 2026-08-24 15:59:11 +08:00
1 parent ca12226b39
commit 5cae5b5c93
159 files changed
+7275 -4718

No files matched your search

+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 } },
{
+16 -10
View File
@@ -150,7 +150,7 @@
if (data && data.vip_url) {
extra = '<a class="btn-primary btn-sm" href="' + escapeAttr(data.vip_url) + '">' + tt('go_vip', '开通会员') + '</a>';
}
} else if (code === 'aksrt_credits_insufficient' || code === 'aksrt_balance_insufficient') {
} else if (code === 'aksrt_points_insufficient' || code === 'aksrt_balance_insufficient') {
title = tt('credit_low', '积分不够');
const balance = typeof (data && data.balance) === 'number' ? data.balance : 0;
const cost = typeof (data && data.cost) === 'number' ? data.cost : 0;
@@ -355,15 +355,17 @@
function showLowCreditModal(button) {
const balance = parseInt(button.getAttribute('data-balance'), 10) || 0;
const cost = parseInt(button.getAttribute('data-cost'), 10) || 0;
renderError('', { balance: balance, cost: cost }, button, 'aksrt_credits_insufficient');
renderError('', { balance: balance, cost: cost }, button, 'aksrt_points_insufficient');
}
// ===== 收银台(内容购买) =====
function restHeaders() {
return {
function restHeaders(idempotencyKey) {
const headers = {
'Content-Type': 'application/json',
'X-WP-Nonce': window.aksrtData.restNonce || '',
};
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
return headers;
}
function payApiBase(apiKind) {
@@ -467,7 +469,7 @@
}
const title = escapeHtml(preview.title || '');
const typeLabel = escapeHtml(preview.pay_type_label || '');
const balance = typeof preview.balance === 'number' ? preview.balance : 0;
const balance = isPoints && Number.isSafeInteger(preview.balance) ? preview.balance : 0;
const methods = Array.isArray(preview.payment_methods) ? preview.payment_methods : [];
const couponAllowed = !isPoints && !!preview.coupon_allowed;
const couponCodeVal = escapeAttr(preview.coupon_code || preview._coupon_code || '');
@@ -477,7 +479,7 @@
priceRows += '<div class="aksrt-cashier__row"><span>' + tt('price', '价格') + '</span><b>' + baseLabel + '</b></div>';
}
// VIP list price row when VIP reduced list amount.
if (typeof preview.list_amount === 'number' && typeof preview.base === 'number' && preview.list_amount !== preview.base) {
if (preview.list_amount !== preview.base) {
priceRows += '<div class="aksrt-cashier__row"><span>' + tt('vip_price', '会员价') + '</span><b class="is-sale">' + listLabel + '</b></div>';
} else if (preview.amount !== preview.base && !preview.rebate_discount && !preview.coupon_discount) {
priceRows += '<div class="aksrt-cashier__row"><span>' + tt('vip_price', '会员价') + '</span><b class="is-sale">' + amountLabel + '</b></div>';
@@ -527,12 +529,12 @@
}
body += '</div>';
}
const walletBal = typeof preview.wallet_balance === 'number'
const walletBal = typeof preview.wallet_balance === 'string'
? preview.wallet_balance
: (typeof preview.balance === 'number' ? preview.balance : 0);
: (typeof preview.balance === 'string' ? preview.balance : '0.00');
const hasBalanceMethod = methods.some(function (m) { return (m && m.id) === 'balance'; });
if (hasBalanceMethod) {
body += '<div class="aksrt-cashier__box"><div class="aksrt-cashier__row"><span>' + tt('my_wallet', '我的余额') + '</span><b class="is-ok">¥' + Number(walletBal).toFixed(2) + '</b></div></div>';
body += '<div class="aksrt-cashier__box"><div class="aksrt-cashier__row"><span>' + tt('my_wallet', '我的余额') + '</span><b class="is-ok">¥' + escapeHtml(walletBal) + '</b></div></div>';
if (preview.balance_enough === false) {
body += '<div class="aksrt-cashier__warn">' + tt('wallet_short', '余额不足,请先充值或选择其他支付方式') + '</div>';
if (preview.credits_url) {
@@ -621,10 +623,13 @@
if (pt) payload.price_type = pt;
if (mode === 'cash' && couponCode) payload.coupon_code = couponCode;
if (!button.dataset.aksrtOperationKey) {
button.dataset.aksrtOperationKey = window.AksrtRequest.createIdempotencyKey();
}
fetch(REST_BASE + payApiBase(apiKind) + postId, {
method: 'POST',
credentials: 'same-origin',
headers: restHeaders(),
headers: restHeaders(button.dataset.aksrtOperationKey),
body: JSON.stringify(payload),
})
.then(function (resp) { return resp.json().then(function (j) { return { ok: resp.ok, status: resp.status, json: j }; }); })
@@ -642,6 +647,7 @@
return;
}
const data = (r.json && r.json.data) ? r.json.data : r.json;
delete button.dataset.aksrtOperationKey;
if (data && data.redirect && (data.payment_url || data.qr_content)) {
if (data.payment_url) {
window.open(data.payment_url, '_blank');
+18
View File
@@ -13,6 +13,23 @@
return window.aksrtData || window.aksrt || {};
}
function createIdempotencyKey() {
if (window.crypto && typeof window.crypto.randomUUID === "function") {
return window.crypto.randomUUID();
}
if (!window.crypto || typeof window.crypto.getRandomValues !== "function") {
throw new Error("当前浏览器无法生成安全的操作标识");
}
var bytes = new Uint8Array(16);
window.crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
var hex = Array.prototype.map.call(bytes, function (value) {
return value.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 restBase() {
var config = data();
var raw = config.restUrl || config.apiUrl || config.rest_url || "";
@@ -144,6 +161,7 @@
window.AksrtRequest = {
url: url,
request: request,
createIdempotencyKey: createIdempotencyKey,
get: function (path, options) {
return request(path, Object.assign({}, options || {}, { method: "GET" }));
},
+71 -21
View File
@@ -16,6 +16,28 @@
}
var activePoll = null;
function operationKey(owner) {
if (!owner.dataset.aksrtOperationKey) {
owner.dataset.aksrtOperationKey = window.AksrtRequest.createIdempotencyKey();
}
return owner.dataset.aksrtOperationKey;
}
function resetOperationKey(owner) {
delete owner.dataset.aksrtOperationKey;
}
function resetOperationKeyOnChange(owner) {
owner.addEventListener('input', function () { resetOperationKey(owner); });
owner.addEventListener('change', function () { resetOperationKey(owner); });
}
function balanceMinor(value) {
var match = /^(0|[1-9]\d*)(?:\.(\d{1,2}))?$/.exec(String(value || '').trim());
if (!match) return null;
return (BigInt(match[1]) * 100n) + BigInt((match[2] || '').padEnd(2, '0') || '0');
}
function api(path, options) {
return window.AksrtRequest.request(path, options || {}).then(function (response) {
return {
@@ -218,6 +240,7 @@
Array.prototype.forEach.call(roots, function (root) {
var tip = root.querySelector('[data-aksrt-card-tip]');
var busy = false;
resetOperationKeyOnChange(root);
function setTip(text, isError) {
if (!tip) return;
@@ -255,6 +278,7 @@
api('/pay/card-pass/redeem', {
method: 'POST',
headers: { 'Idempotency-Key': operationKey(root) },
body: JSON.stringify(body)
}).then(function (r) {
busy = false;
@@ -264,6 +288,7 @@
return;
}
var data = (r.json && r.json.data) ? r.json.data : r.json;
resetOperationKey(root);
if (type === 'vip' && window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === 'function') {
window.AksrtModalRuntime.invalidate(['vip', 'orders']);
}
@@ -278,7 +303,7 @@
});
}
// ===== 余额充值 / 转账 / 积分兑余额(legacy 资产中心) =====
// ===== 余额充值 / 转账 / 余额兑积分 =====
function openModal(el) {
if (!el) return;
el.hidden = false;
@@ -344,21 +369,23 @@
});
});
if (!form) return;
resetOperationKeyOnChange(form);
form.addEventListener('submit', function (e) {
e.preventDefault();
if (busy) return;
var pkgInput = form.querySelector('input[name="balance_package"]:checked') || form.querySelector('input[name="balance_package"]');
var pkg = pkgInput ? pkgInput.value : 'custom';
var customEl = form.querySelector('input[name="custom_price"]');
var custom = customEl ? parseFloat(customEl.value || '0') : 0;
if (custom > 0) {
var custom = customEl ? String(customEl.value || '').trim() : '';
var validCustom = /^(0|[1-9]\d*)(?:\.\d{1,2})?$/.test(custom) && custom !== '0' && custom !== '0.0' && custom !== '0.00';
if (validCustom) {
pkg = 'custom';
}
if ((!pkg || pkg === 'custom') && !(custom > 0) && !pkgInput) {
if ((!pkg || pkg === 'custom') && !validCustom && !pkgInput) {
notyf('请选择或输入充值金额', 'danger');
return;
}
if (pkg === 'custom' && !(custom > 0)) {
if (pkg === 'custom' && !validCustom) {
notyf('请输入有效充值金额', 'danger');
return;
}
@@ -368,6 +395,7 @@
notyf('正在创建充值订单…', 'load', '', 'aksrt-balance-charge-toast');
api('/pay/balance/charge', {
method: 'POST',
headers: { 'Idempotency-Key': operationKey(form) },
body: JSON.stringify({
package: pkg,
custom_price: custom,
@@ -380,6 +408,7 @@
return;
}
var data = (r.json && r.json.data) ? r.json.data : r.json;
resetOperationKey(form);
if (data && data.redirect && (data.payment_url || data.qr_content)) {
if (data.payment_url) window.open(data.payment_url, '_blank');
notyf(data.message || '请完成支付,支付成功后将自动刷新。', 'info', 5, 'aksrt-balance-charge-toast');
@@ -478,6 +507,7 @@
});
}
if (!form) return;
resetOperationKeyOnChange(form);
form.addEventListener('submit', function (e) {
e.preventDefault();
if (busy) return;
@@ -489,7 +519,12 @@
notyf('请选择收款用户', 'danger');
return;
}
if (!(parseFloat(amount) > 0) || (type === 'points' && (!Number.isInteger(Number(amount)) || Number(amount) < pointsTransferMin))) {
var pointsAmount = /^[1-9]\d*$/.test(String(amount)) ? BigInt(String(amount)) : 0n;
var cashAmount = balanceMinor(amount);
var validAmount = type === 'points'
? pointsAmount >= BigInt(pointsTransferMin) && pointsAmount <= BigInt(Number.MAX_SAFE_INTEGER)
: cashAmount !== null && cashAmount > 0n;
if (!validAmount) {
notyf('请输入有效金额', 'danger');
return;
}
@@ -497,7 +532,12 @@
notyf('正在转账…', 'load', '', 'aksrt-balance-transfer-toast');
api('/pay/transfer', {
method: 'POST',
body: JSON.stringify({ type: type, recipient: recipient, amount: amount })
headers: { 'Idempotency-Key': operationKey(form) },
body: JSON.stringify({
type: type,
recipient: recipient,
amount: type === 'points' ? Number(pointsAmount) : String(amount)
})
}).then(function (r) {
busy = false;
if (!r.ok || (r.json && r.json.code && !r.json.success)) {
@@ -505,6 +545,7 @@
return;
}
var data = (r.json && r.json.data) ? r.json.data : r.json;
resetOperationKey(form);
notyf((data && data.message) || '转账成功,正在刷新…', 'success', 5, 'aksrt-balance-transfer-toast');
window.setTimeout(function () { window.location.reload(); }, 800);
}).catch(function () {
@@ -529,12 +570,15 @@
btn.addEventListener('click', function () { closeModal(modal); });
});
if (!form) return;
resetOperationKeyOnChange(form);
function updatePreview() {
if (!preview || !amountEl) return;
var amount = Number(amountEl.value || '0');
var rate = Number(amountEl.getAttribute('data-aksrt-points-exchange-rate') || '0');
preview.textContent = Number.isInteger(amount) && amount > 0 && rate > 0
? '预计获得 ' + (amount * rate) + ' 积分'
var amount = balanceMinor(amountEl.value);
var rateRaw = amountEl.getAttribute('data-aksrt-points-exchange-rate') || '';
var rate = /^[1-9]\d*$/.test(rateRaw) ? BigInt(rateRaw) : 0n;
var scaled = amount !== null ? amount * rate : 0n;
preview.textContent = amount !== null && amount > 0n && rate > 0n && scaled % 100n === 0n
? '预计获得 ' + String(scaled / 100n) + ' 积分'
: '';
}
amountEl.addEventListener('input', updatePreview);
@@ -542,15 +586,16 @@
form.addEventListener('submit', function (e) {
e.preventDefault();
if (busy) return;
var amount = amountEl ? Number(amountEl.value || '0') : 0;
if (!Number.isInteger(amount) || !(amount > 0)) {
notyf('请输入正整数余额', 'danger');
var amount = amountEl ? String(amountEl.value || '').trim() : '';
if (!/^(0|[1-9]\d*)(?:\.\d{1,2})?$/.test(amount) || /^0(?:\.0{1,2})?$/.test(amount)) {
notyf('请输入最多两位小数的正数余额', 'danger');
return;
}
busy = true;
notyf('正在购买积分…', 'load', '', 'aksrt-balance-points-toast');
api('/pay/balance/points-exchange', {
method: 'POST',
headers: { 'Idempotency-Key': operationKey(form) },
body: JSON.stringify({ amount: amount })
}).then(function (r) {
busy = false;
@@ -559,6 +604,7 @@
return;
}
var data = (r.json && r.json.data) ? r.json.data : r.json;
resetOperationKey(form);
notyf((data && data.message) || '购买成功,正在刷新…', 'success', 5, 'aksrt-balance-points-toast');
window.setTimeout(function () { window.location.reload(); }, 800);
}).catch(function () {
@@ -581,6 +627,7 @@
btn.addEventListener('click', function () { closeModal(modal); });
});
if (!form) return;
resetOperationKeyOnChange(form);
form.addEventListener('submit', function (e) {
e.preventDefault();
if (busy) return;
@@ -588,17 +635,18 @@
var methodEl = form.querySelector('[data-aksrt-withdraw-method]');
var accountEl = form.querySelector('[data-aksrt-withdraw-account]');
var rawAmount = amountEl ? String(amountEl.value || '').trim() : '';
var amount = rawAmount ? Number(rawAmount) : 0;
var minAmount = amountEl ? Number(amountEl.getAttribute('data-withdraw-min') || '0') : 0;
var remainingAmount = amountEl ? Number(amountEl.getAttribute('data-withdraw-remaining-amount') || '0') : 0;
var amount = rawAmount;
var amountMinor = balanceMinor(amount);
var minAmount = amountEl ? balanceMinor(amountEl.getAttribute('data-withdraw-min') || '0') : 0n;
var remainingAmount = amountEl ? balanceMinor(amountEl.getAttribute('data-withdraw-remaining-amount') || '0') : 0n;
var remainingCount = amountEl ? Number(amountEl.getAttribute('data-withdraw-remaining-count') || '0') : 0;
var method = methodEl ? methodEl.value : 'alipay';
var account = accountEl ? String(accountEl.value || '').trim() : '';
if (!Number.isInteger(amount) || !(amount > 0)) {
notyf('提现金额必须为正整数', 'danger');
if (amountMinor === null || amountMinor <= 0n) {
notyf('提现金额必须为最多两位小数的正数', 'danger');
return;
}
if (amount < minAmount) {
if (minAmount === null || amountMinor < minAmount) {
notyf('提现金额低于最低提现门槛', 'danger');
return;
}
@@ -606,7 +654,7 @@
notyf('当日提现次数已用尽', 'danger');
return;
}
if (amount > remainingAmount) {
if (remainingAmount === null || amountMinor > remainingAmount) {
notyf('本次提现后将超出单日提现总额上限', 'danger');
return;
}
@@ -618,6 +666,7 @@
notyf('正在提交提现申请…', 'load', '', 'aksrt-balance-withdraw-toast');
api('/earnings/withdrawals', {
method: 'POST',
headers: { 'Idempotency-Key': operationKey(form) },
body: JSON.stringify({ amount: amount, method: method, account: account })
}).then(function (r) {
busy = false;
@@ -626,6 +675,7 @@
return;
}
var data = (r.json && r.json.data) ? r.json.data : r.json;
resetOperationKey(form);
notyf((data && data.message) || '申请已提交', 'success', 5, 'aksrt-balance-withdraw-toast');
closeModal(modal);
var panel = modal.closest ? modal.closest('.dashboard-panel') : null;
+33 -16
View File
@@ -8,9 +8,10 @@
var instances = new WeakMap();
function request(path, body, method) {
function request(path, body, method, operationKey) {
var options = { method: method || "POST" };
if (body !== undefined) options.body = JSON.stringify(body || {});
if (operationKey) options.headers = { "Idempotency-Key": operationKey };
return window.AksrtRequest.request(path, options);
}
@@ -30,12 +31,15 @@
.replace(/'/g, "&#39;");
}
function formatMoney(order) {
var amount = Number(order && order.amount || 0);
return order && order.is_points
? escapeHtml(order.currency_symbol || "积分") + " " + Math.round(amount)
: "¥" + amount.toFixed(2);
}
function formatMoney(order) {
var raw = String(order && order.amount != null ? order.amount : "0");
if (order && order.is_points) {
return escapeHtml(order.currency_symbol || "积分") + " " + (/^\d+$/.test(raw) ? raw : "0");
}
var match = /^(0|[1-9]\d*)(?:\.(\d{1,2}))?$/.exec(raw);
var amount = match ? match[1] + "." + (match[2] || "").padEnd(2, "0") : "0.00";
return "¥" + amount;
}
function orderTag(orderNumber) {
return "order:" + String(orderNumber || "").toLowerCase();
@@ -100,9 +104,12 @@
}
function detailOptions(state, orderNumber, refresh) {
var actionKey = refresh ? "refresh:" + orderNumber : "";
if (actionKey && !state.operationKeys[actionKey]) state.operationKeys[actionKey] = window.AksrtRequest.createIdempotencyKey();
return {
url: orderPath(orderNumber, refresh ? "/refresh" : ""),
method: refresh ? "POST" : "GET",
headers: actionKey ? { "Idempotency-Key": state.operationKeys[actionKey] } : {},
kind: "orders",
title: "订单详情",
responseType: "json",
@@ -123,7 +130,10 @@
renderDetail(state, data, session);
},
onData: function (data) {
if (refresh) invalidateOrder(orderNumber, data);
if (refresh) {
delete state.operationKeys[actionKey];
invalidateOrder(orderNumber, data);
}
},
};
}
@@ -244,12 +254,15 @@
});
}
function startPayment(state, button) {
function startPayment(state, button) {
var number = button.getAttribute("data-aksrt-order-pay") || "";
var method = button.getAttribute("data-aksrt-order-method") || "";
if (!number || button.disabled) return;
button.disabled = true;
request(orderPath(number, "/pay"), method ? { payment_method: method } : {}, "POST").then(function (result) {
if (!number || button.disabled) return;
var actionKey = "pay:" + number;
if (!state.operationKeys[actionKey]) state.operationKeys[actionKey] = window.AksrtRequest.createIdempotencyKey();
button.disabled = true;
request(orderPath(number, "/pay"), method ? { payment_method: method } : {}, "POST", state.operationKeys[actionKey]).then(function (result) {
delete state.operationKeys[actionKey];
var data = result.data || {};
invalidateOrder(number, data.order);
if (data.redirect && data.payment_url) {
@@ -273,8 +286,11 @@
button.textContent = "确认关闭";
return;
}
button.disabled = true;
request(orderPath(number, "/cancel"), {}, "POST").then(function (result) {
button.disabled = true;
var actionKey = "cancel:" + number;
if (!state.operationKeys[actionKey]) state.operationKeys[actionKey] = window.AksrtRequest.createIdempotencyKey();
request(orderPath(number, "/cancel"), {}, "POST", state.operationKeys[actionKey]).then(function (result) {
delete state.operationKeys[actionKey];
var data = result.data || {};
invalidateOrder(number, data.order);
notyf("订单已关闭", "success");
@@ -294,13 +310,14 @@
function initRoot(root) {
if (instances.has(root)) return;
var state = {
var state = {
root: root,
detailOrderNumber: "",
detailSession: null,
detailReplacement: false,
detailReplacementSession: null,
countdownTimer: null,
countdownTimer: null,
operationKeys: {},
};
state.onRootClick = function (event) {
+17 -1
View File
@@ -11,6 +11,13 @@
function initReward() {
var activeTrigger = null;
function operationKey(modal) {
if (!modal.dataset.aksrtOperationKey) {
modal.dataset.aksrtOperationKey = window.AksrtRequest.createIdempotencyKey();
}
return modal.dataset.aksrtOperationKey;
}
function closeAll() {
document.querySelectorAll('.reward-modal.is-open').forEach(function (modal) {
modal.classList.remove('is-open');
@@ -52,6 +59,7 @@
var amountButton = event.target.closest('[data-reward-amount]');
if (amountButton) {
var modal = amountButton.closest('.reward-modal');
delete modal.dataset.aksrtOperationKey;
modal.querySelectorAll('[data-reward-amount]').forEach(function (button) {
button.classList.toggle('is-active', button === amountButton);
});
@@ -67,6 +75,10 @@
var customInput = tipModal.querySelector('[data-aksrt-reward-custom]');
var selected = tipModal.querySelector('[data-reward-amount].is-active');
var amount = customInput && customInput.value ? customInput.value : (selected ? selected.dataset.rewardAmount : '');
if (customInput && customInput.dataset.aksrtIdempotencyBound !== '1') {
customInput.dataset.aksrtIdempotencyBound = '1';
customInput.addEventListener('input', function () { delete tipModal.dataset.aksrtOperationKey; });
}
submit.disabled = true;
submit.textContent = '赞赏中…';
@@ -82,13 +94,17 @@
fetch(cfg.ajaxUrl || '/wp-admin/admin-ajax.php', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Idempotency-Key': operationKey(tipModal)
},
body: body.toString()
}).then(function (response) {
return response.json();
}).then(function (response) {
var text = response && response.data && response.data.message ? response.data.message : '赞赏失败';
if (response && response.success) {
delete tipModal.dataset.aksrtOperationKey;
if (message) message.textContent = text;
setTimeout(closeAll, 900);
} else if (message) {
+26 -2
View File
@@ -75,6 +75,11 @@
var refreshTimeout = null;
var refreshInFlight = false;
var openSequence = 0;
var pendingCreateKey = '';
var pendingCreateSignature = '';
var pendingRefreshKey = '';
var pendingManualKey = '';
var pendingManualSignature = '';
function isOpen(shell) {
return !!shell && shell.classList.contains('is-open') && document.contains(shell);
@@ -176,6 +181,7 @@
var timedOut = false;
var shell = activeShell;
var timeoutMs = Math.max(5000, Number(config.refreshTimeout || 15000));
if (!pendingRefreshKey) pendingRefreshKey = uuid();
refreshTimeout = window.setTimeout(function () {
timedOut = true;
if (refreshController) {
@@ -185,8 +191,10 @@
return request(config.restBase + '/' + encodeURIComponent(currentUuid) + '/refresh', config, {
method: 'POST',
headers: { 'Idempotency-Key': pendingRefreshKey },
signal: refreshController ? refreshController.signal : undefined,
}).then(function (data) {
pendingRefreshKey = '';
if (shell !== activeShell || !isOpen(shell)) return data;
pollBackoff = explicit ? 0 : Math.min(4, pollBackoff + 1);
render(data, shell);
@@ -270,17 +278,23 @@
var payload = {};
form.forEach(function (value, key) { payload[key] = value; });
payload.consent = payload.consent === '1';
payload.client_request_id = uuid();
var signature = JSON.stringify(payload);
if (!pendingCreateKey || pendingCreateSignature !== signature) {
pendingCreateKey = uuid();
pendingCreateSignature = signature;
}
if (button) button.disabled = true;
if (message) message.textContent = '';
aliyunMetaInfo(config).then(function (metaInfo) {
if (metaInfo) payload.meta_info = metaInfo;
return request(config.restBase, config, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': pendingCreateKey },
body: JSON.stringify(payload),
});
}).then(function (data) {
pendingCreateKey = '';
pendingCreateSignature = '';
render(data, shell);
}).catch(function (error) {
if (message) message.textContent = errorMessage(error, '创建认证会话失败,请重试。');
@@ -300,11 +314,21 @@
var button = manualForm.querySelector('[type="submit"]');
var body = new FormData();
Array.prototype.forEach.call((files && files.files) || [], function (file) { body.append('evidence[]', file); });
var signature = Array.prototype.map.call((files && files.files) || [], function (file) {
return [file.name, file.size, file.lastModified].join(':');
}).join('|');
if (!pendingManualKey || pendingManualSignature !== signature) {
pendingManualKey = uuid();
pendingManualSignature = signature;
}
if (button) button.disabled = true;
request(config.restBase + '/' + encodeURIComponent(currentUuid) + '/manual', config, {
method: 'POST',
headers: { 'Idempotency-Key': pendingManualKey },
body: body,
}).then(function (data) {
pendingManualKey = '';
pendingManualSignature = '';
manualForm.hidden = true;
render(data, shell);
}).catch(function (error) {
+1 -5
View File
@@ -89,10 +89,6 @@
$(this).text(aksrt__('admin_menu_title_income'));
}
if (url.endsWith('aksrtpay_rebate_page')) {
$(this).text(aksrt__('admin_menu_title_card'));
}
if (url.endsWith('aksrtpay_product_page')) {
$(this).text(aksrt__('admin_menu_title_product'));
}
@@ -448,6 +444,6 @@
$('.permalink-structure [name="permalink_structure"]').val('/%post_id%.html').click();
});
console.log('\n' + ' %c legacy Theme %c https://legacy.com ' + '\n', 'color: #fadfa3; background: #030307; padding:3px; font-size:12px;', 'color: #2abd4e;background: #16171a; padding: 3px; font-size: 12px;');
console.log('%c legacy Theme %c https://legacy.com', 'color: #fadfa3; background: #030307; padding:3px; font-size:12px;', 'color: #2abd4e;background: #16171a; padding: 3px; font-size: 12px;');
});
})(jQuery, document);
+1 -1
View File
@@ -1 +1 @@
(function(t,e){function i(t){var e=window.aksrt_admin_main_var&&window.aksrt_admin_main_var.i18n;return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:t}function n(t){var e=Array.prototype.slice.call(arguments,1),n=i(t),a=0;return n.replace(/%(\d+\$)?s/g,function(t,i){var n=i?parseInt(i,10)-1:a++;return void 0!==e[n]&&null!==e[n]?e[n]:t})}function a(t,e,i,n,a){var s,r,d;n&&(e+="px",i+="px",s="translate3D("+e+","+i+" , 0)",r={},d=o(),r[d+"transform"]=s,r[d+"transition"]=d+"transform 0s linear",r.cursor=a,"null"==n&&(r[d+"transform"]="",r[d+"transition"]=""),t.css(r))}function o(){var t=e.body||e.documentElement;return t=t.style,""==t.WebkitTransition?"-webkit-":""==t.MozTransition?"-moz-":""==t.OTransition?"-o-":""==t.transition?"":void 0}t.fn.minitouch=function(e){e=t.extend({direction:"bottom",selector:"",depreciation:50,onStart:!1,onEnd:!1},e);var i=t(this),n=(t("body"),e.depreciation),o=0,s=0,r=0,d=0,l=0,c=0,p=0,m=!1;i.on("touchstart pointerdown MSPointerDown",e.selector,function(t){o=0,s=0,r=0,d=0,l=0,c=0,p=0,o=t.originalEvent.pageX||t.originalEvent.touches[0].pageX,s=t.originalEvent.pageY||t.originalEvent.touches[0].pageY,m=!0}).on("touchmove pointermove MSPointerMove",e.selector,function(i){r=i.originalEvent.pageX||i.originalEvent.touches[0].pageX,d=i.originalEvent.pageY||i.originalEvent.touches[0].pageY,c=r-o,p=d-s,l=180*Math.atan2(p,c)/Math.PI,"right"==e.direction&&(p=0,c=l>-40&&l<40&&c>0?c:0),"left"==e.direction&&(p=0,c=(l>150||l<-150)&&0>c?c:0),"top"==e.direction&&(c=0,p=l>-130&&l<-50&&0>p?p:0),"bottom"==e.direction&&(c=0,p=l>50&&l<130&&p>0?p:0),0===c&&0===p||(i.preventDefault(),a(t(this),c,p,m,"grab"))}).on("touchend touchcancel pointerup MSPointerUp",e.selector,function(){(Math.abs(c)>n||Math.abs(p)>n)&&0!=e.onEnd&&e.onEnd(i),a(t(this),0,0,"null",""),m=!1,o=0,s=0,r=0,d=0,l=0,c=0,p=0})},t(e).ready(function(t){function a(e,i,n,a){t(".notyn").length||l.append('<div class="notyn"></div>'),i=i||"success",n=n||5e3,n=n<100?1e3*n:n;var s=a?' id="'+a+'"':"",r=t('<div class="noty1"'+s+'><div class="notyf '+i+'">'+e+"</div></div>"),d=!a;a&&t("#"+a).length?(t("#"+a).find(".notyf").removeClass().addClass("notyf "+i).html(e),r=t("#"+a),d=!0):t(".notyn").append(r),d&&setTimeout(function(){o(r)},n)}function o(t){t.addClass("notyn-out"),setTimeout(function(){t.remove()},1e3)}function s(i,n,a,o){function s(t,e,i){if(t.createTextRange){var n=t.createTextRange();n.collapse(!0),n.moveStart("character",e),n.moveEnd("character",i-e),n.select()}else t.setSelectionRange(e,i),t.select()}var r=i.toString(),d=e.querySelector("#copy-input");d||(d=e.createElement("input"),d.id="copy-input",d.readOnly="readOnly",d.style.position="fixed",d.style.left="-2000px",d.style.zIndex="-1000",o.parentNode.appendChild(d)),d.value=r,s(d,0,r.length),e.execCommand("copy")?t.isFunction(n)&&n():t.isFunction(a)&&a(),d.blur()}var r=t("#toplevel_page_aksrtpay_page");if(r.length&&"zh-Hans"!==t("html").attr("lang")){r.find(".toplevel_page_aksrtpay_page .wp-menu-name").text(i("admin_menu_title_shop_center"));var d=r.find(".wp-submenu");d.find("a.wp-first-item").text(i("admin_menu_title_shop_center")),d.find("a").each(function(){var e=t(this).attr("href");e.endsWith("aksrtpay_page")&&t(this).text(i("admin_menu_title_shop_center")),e.endsWith("aksrtpay_page#/order")&&t(this).text(i("admin_menu_title_order")),e.endsWith("aksrtpay_page#/shipping")&&t(this).text(i("admin_menu_title_shipping")),e.endsWith("aksrtpay_page#/after-sale")&&t(this).text(i("admin_menu_title_after_sale")),e.endsWith("aksrtpay_income_page")&&t(this).text(i("admin_menu_title_income")),e.endsWith("aksrtpay_rebate_page")&&t(this).text(i("admin_menu_title_card")),e.endsWith("aksrtpay_product_page")&&t(this).text(i("admin_menu_title_product")),e.endsWith("aksrtpay_charge_card_page")&&t(this).text(i("admin_menu_title_card")),e.endsWith("aksrtpay_coupon_page")&&t(this).text(i("admin_menu_title_coupon")),e.endsWith("aksrtpay_withdraw")&&t(this).text(i("admin_menu_title_withdraw")),e.endsWith("users.php")&&t(this).text(i("admin_menu_title_vip")),e.endsWith("aksrtpay_order_page")&&t(this).text(i("admin_menu_title_order_old"))})}var l=t("body");"function"!=typeof t.fn.serializeObject&&(t.fn.serializeObject=function(){var e={},i=this.serializeArray();return t.each(i,function(){void 0!==e[this.name]?(e[this.name].push||(e[this.name]=[e[this.name]]),e[this.name].push(this.value||"")):e[this.name]=this.value||""}),e}),l.width()<783&&t("#adminmenuwrap").minitouch({direction:"left",onEnd:function(){t("#wpwrap").removeClass("wp-responsive-open")}}),l.on("click",".noty1",function(){o(t(this))}),l.on("click","[data-clipboard-text]",function(){var e=t(this),o=e.attr("data-clipboard-text"),r=e.attr("data-clipboard-tag")||i("admin_content");s(o,function(){a(n("admin_copied",r))},function(){a(n("admin_copy_failed",r),"danger")},this)}),l.on("click",'[data-toggle="RefreshModal"]',function(){var e=t(this),i=e.attr("data-class")||"modal-mini",n=e.attr("data-remote"),a=e.attr("data-height")||200,o=e.attLine truncated
(function(k,w){function e(t){var c=window.aksrt_admin_main_var&&window.aksrt_admin_main_var.i18n;return c&&Object.prototype.hasOwnProperty.call(c,t)?c[t]:t}function M(t){var c=Array.prototype.slice.call(arguments,1),g=e(t),n=0;return g.replace(/%(\d+\$)?s/g,function(v,h){var d=h?parseInt(h,10)-1:n++;return c[d]!==void 0&&c[d]!==null?c[d]:v})}function W(t,c,g,n,v){var h,d,r;n&&(c+="px",g+="px",h="translate3D("+c+","+g+" , 0)",d={},r=P(),d[r+"transform"]=h,d[r+"transition"]=r+"transform 0s linear",d.cursor=v,n=="null"&&(d[r+"transform"]="",d[r+"transition"]=""),t.css(d))}function P(){var t=w.body||w.documentElement;return t=t.style,t.WebkitTransition==""?"-webkit-":t.MozTransition==""?"-moz-":t.OTransition==""?"-o-":t.transition==""?"":void 0}k.fn.minitouch=function(t){t=k.extend({direction:"bottom",selector:"",depreciation:50,onStart:!1,onEnd:!1},t);var c=k(this),g=(k("body"),t.depreciation),n=0,v=0,h=0,d=0,r=0,p=0,l=0,x=!1;c.on("touchstart pointerdown MSPointerDown",t.selector,function(_){n=0,v=0,h=0,d=0,r=0,p=0,l=0,n=_.originalEvent.pageX||_.originalEvent.touches[0].pageX,v=_.originalEvent.pageY||_.originalEvent.touches[0].pageY,x=!0}).on("touchmove pointermove MSPointerMove",t.selector,function(_){h=_.originalEvent.pageX||_.originalEvent.touches[0].pageX,d=_.originalEvent.pageY||_.originalEvent.touches[0].pageY,p=h-n,l=d-v,r=180*Math.atan2(l,p)/Math.PI,t.direction=="right"&&(l=0,p=r>-40&&r<40&&p>0?p:0),t.direction=="left"&&(l=0,p=(r>150||r<-150)&&0>p?p:0),t.direction=="top"&&(p=0,l=r>-130&&r<-50&&0>l?l:0),t.direction=="bottom"&&(p=0,l=r>50&&r<130&&l>0?l:0),p===0&&l===0||(_.preventDefault(),W(k(this),p,l,x,"grab"))}).on("touchend touchcancel pointerup MSPointerUp",t.selector,function(){(Math.abs(p)>g||Math.abs(l)>g)&&t.onEnd!=0&&t.onEnd(c),W(k(this),0,0,"null",""),x=!1,n=0,v=0,h=0,d=0,r=0,p=0,l=0})},k(w).ready(function(t){var c=t("#toplevel_page_aksrtpay_page");if(c.length&&t("html").attr("lang")!=="zh-Hans"){c.find(".toplevel_page_aksrtpay_page .wp-menu-name").text(e("admin_menu_title_shop_center"));var g=c.find(".wp-submenu");g.find("a.wp-first-item").text(e("admin_menu_title_shop_center")),g.find("a").each(function(){var a=t(this).attr("href");a.endsWith("aksrtpay_page")&&t(this).text(e("admin_menu_title_shop_center")),a.endsWith("aksrtpay_page#/order")&&t(this).text(e("admin_menu_title_order")),a.endsWith("aksrtpay_page#/shipping")&&t(this).text(e("admin_menu_title_shipping")),a.endsWith("aksrtpay_page#/after-sale")&&t(this).text(e("admin_menu_title_after_sale")),a.endsWith("aksrtpay_income_page")&&t(this).text(e("admin_menu_title_income")),a.endsWith("aksrtpay_product_page")&&t(this).text(e("admin_menu_title_product")),a.endsWith("aksrtpay_charge_card_page")&&t(this).text(e("admin_menu_title_card")),a.endsWith("aksrtpay_coupon_page")&&t(this).text(e("admin_menu_title_coupon")),a.endsWith("aksrtpay_withdraw")&&t(this).text(e("admin_menu_title_withdraw")),a.endsWith("users.php")&&t(this).text(e("admin_menu_title_vip")),a.endsWith("aksrtpay_order_page")&&t(this).text(e("admin_menu_title_order_old"))})}var n=t("body");typeof t.fn.serializeObject!="function"&&(t.fn.serializeObject=function(){var a={},s=this.serializeArray();return t.each(s,function(){a[this.name]!==void 0?(a[this.name].push||(a[this.name]=[a[this.name]]),a[this.name].push(this.value||"")):a[this.name]=this.value||""}),a}),n.width()<783&&t("#adminmenuwrap").minitouch({direction:"left",onEnd:function(){t("#wpwrap").removeClass("wp-responsive-open")}});function v(a,s,i,m){t(".notyn").length||n.append('<div class="notyn"></div>'),s=s||"success",i=i||5e3,i=i<100?i*1e3:i;var f=m?' id="'+m+'"':"",o=t('<div class="noty1"'+f+'><div class="notyf '+s+'">'+a+"</div></div>"),b=!m;m&&t("#"+m).length?(t("#"+m).find(".notyf").removeClass().addClass("notyf "+s).html(a),o=t("#"+m),b=!0):t(".notyn").append(o),b&&setTimeout(function(){h(o)},i)}function h(a){a.addClass("notyn-out"),setTimeout(function(){a.remove()},1e3)}n.on("click",".noty1",function(){h(t(this))});function d(a,s,i,m){var f=a.toString(),o=w.querySelector("#copy-input");o||(o=w.createElement("input"),o.id="copy-input",o.readOnly="readOnly",o.style.position="fixed",o.style.left="-2000px",o.style.zIndex="-1000",m.parentNode.appendChild(o)),o.value=f,b(o,0,f.length),w.execCommand("copy")?t.isFunction(s)&&s():t.isFunction(i)&&i(),o.blur();function b(y,C,E){if(y.createTextRange){var T=y.createTextRange();T.collapse(!0),T.moveStart("character",C),T.moveEnd("character",E-C),T.select()}else y.setSelectionRange(C,E),y.select()}}n.on("click","[data-clipboard-text]",function(){var a=t(this),s=a.attr("data-clipboard-text"),i=a.attr("data-clipboard-tag")||e("admin_content");d(s,function(){v(M("admin_copied",i))},function(){v(M("admin_copy_failed",i),"danger")},this)}),n.on("click",'[data-toggle="RefreshModal"]',function(){var a=t(this),s=a.attr("data-class")||"modal-mini",i=a.attr("data-remote"),m=a.attr("data-height")||200,f=a.attr("mobile-bottom")&&t(window).width()<769?" bottom":"",o="aksrt-modal flex jLine truncated
+15 -2
View File
@@ -15,6 +15,7 @@
var DEFAULT_MAX_ELAPSED = 120000;
var MAX_DELAY = 15000;
var createFlights = Object.create(null);
var operationKeys = Object.create(null);
var polls = Object.create(null);
var lastScope = "";
var scopeGeneration = 0;
@@ -47,6 +48,7 @@
if (flight && typeof flight.cancel === "function") flight.cancel();
});
createFlights = Object.create(null);
operationKeys = Object.create(null);
Object.keys(polls).forEach(function (pollKey) {
var active = polls[pollKey];
if (active && active.handle && typeof active.handle.abort === "function") active.handle.abort();
@@ -115,6 +117,7 @@
var key = userScope() + ":" + String(packageId) + ":" + method;
if (createFlights[key]) return createFlights[key].promise;
if (!operationKeys[key]) operationKeys[key] = window.AksrtRequest.createIdempotencyKey();
var requestController = !options.signal && window.AbortController
? new window.AbortController()
@@ -122,6 +125,7 @@
var flight = {
promise: request("/pay/vip", {
method: "POST",
headers: { "Idempotency-Key": operationKeys[key] },
body: JSON.stringify({
package_id: packageId,
payment_method: method,
@@ -143,22 +147,31 @@
}, function () {
if (createFlights[key] === flight) delete createFlights[key];
});
flight.promise.then(function (response) {
if (response.ok) delete operationKeys[key];
});
return flight.promise;
}
function exchange(options) {
options = options || {};
userScope();
var scope = userScope();
var packageId = parseInt(options.packageId || options.package_id || 0, 10);
if (!packageId) {
return Promise.resolve({ ok: false, status: 400, json: { message: "兑换套餐无效" } });
}
var key = scope + ":points-exchange:" + String(packageId);
if (!operationKeys[key]) operationKeys[key] = window.AksrtRequest.createIdempotencyKey();
return request("/pay/vip/points-exchange", {
method: "POST",
headers: { "Idempotency-Key": operationKeys[key] },
body: JSON.stringify({ package_id: packageId }),
signal: options.signal,
}).then(function (response) {
if (response.ok) invalidate(options.tags || ["vip", "orders"]);
if (response.ok) {
delete operationKeys[key];
invalidate(options.tags || ["vip", "orders"]);
}
return response;
});
}