fix: 回调配置, 调整信用位置, 套餐
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m55s

This commit is contained in:
luo
2026-07-29 18:20:42 +08:00
parent 4394ae0f78
commit 17d2eeebc5
39 changed files with 2687 additions and 1532 deletions

View File

@@ -53,6 +53,7 @@ export const buildAgentRechargeActions = (
if (
!hasApprovalRecord &&
row.status === AgentRechargeStatus.PENDING &&
row.payment_method === 'offline' &&
options.hasAuth('agent_recharge:reject')
) {
actions.push({

View File

@@ -0,0 +1,23 @@
import { normalizeApiError } from '@/utils/business/apiError'
import type { AgentRechargeStatus } from '@/types/api'
export const ONLINE_RECHARGE_TERMINAL_STATUSES: AgentRechargeStatus[] = [3, 4, 5, 6]
export const isOnlineRechargeTerminal = (status: AgentRechargeStatus): boolean => {
return ONLINE_RECHARGE_TERMINAL_STATUSES.includes(status)
}
export const createOnlineRechargeRequestId = (): string => {
const randomId = globalThis.crypto?.randomUUID?.()
const suffix = randomId || Math.random().toString(36).slice(2, 12)
return `recharge-${Date.now()}-${suffix}`
}
export const amountYuanToFen = (amount: number): number => {
return Math.round(amount * 100)
}
export const shouldRetryOnlineCreate = (error: unknown): boolean => {
const kind = normalizeApiError(error).kind
return kind === 'timeout' || kind === 'server' || kind === 'unknown'
}

View File

@@ -78,11 +78,19 @@
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
const methodMap: Record<AgentRechargePaymentMethod, string> = {
wechat: '微信在线支付',
alipay: '支付宝在线支付',
offline: '线下转账'
}
return methodMap[method] || method
}
const getRechargeSourceText = (data: AgentRecharge): string => {
if (data.recharge_source_name) return data.recharge_source_name
if (data.recharge_source === 'agent_online') return '代理在线自充'
if (data.recharge_source === 'platform_offline') return '平台线下代充'
return '-'
}
const getApprovalProviderText = (data: AgentRecharge) => {
if (data.approval_provider === 'wecom' || data.approval_source === 'wecom') return '企微'
if (data.approval_source === 'legacy') return '历史审批'
@@ -90,124 +98,155 @@
}
// 详情配置
const detailSections = computed((): DetailSection[] => [
{
title: '订单信息',
fields: [
{ label: '充值单号', prop: 'recharge_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '提交人', prop: 'submitter_name', formatter: (value) => value || '-' },
{
label: '充值金额',
formatter: (_, data) => formatCurrency(data.amount)
},
{
label: '状态',
render: (data) =>
h(ElTag, { type: getStatusType(data.status) }, () => data.status_name || '-')
},
{
label: '驳回原因',
prop: 'rejection_reason',
formatter: (value) => formatRejectionReason(value),
fullWidth: true
}
]
},
{
title: '企微审批信息',
fields: [
{ label: '审批渠道', formatter: (_, data) => getApprovalProviderText(data) },
{
label: '审批状态',
formatter: (_, data) => data.approval_status_name || '-'
},
{
label: '当前审批人摘要',
formatter: (_, data) => data.current_approver_summary || '-',
fullWidth: true
}
]
},
{
title: '业务处理结果',
fields: [
{
label: '处理状态',
formatter: (_, data) => data.processing_status_name || '-'
}
]
},
{
title: '支付信息',
fields: [
{
label: '支付方式',
formatter: (_, data) => getPaymentMethodText(data.payment_method)
},
{
label: '支付通道',
prop: 'payment_channel',
formatter: (value) => value || '-'
},
{
label: '第三方支付流水号',
prop: 'payment_transaction_id',
formatter: (value) => value || '-',
fullWidth: true
},
{
label: '支付凭证',
fullWidth: true,
render: (data) =>
hasVoucherKeys(data.payment_voucher_key)
? h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => {
paymentVoucherFileKeys.value = toVoucherKeyList(data.payment_voucher_key)
}
},
() => '查看支付凭证'
)
: h('span', '-')
},
{
label: '运营备注',
prop: 'remark',
formatter: (value) => value || '-',
fullWidth: true
}
]
},
{
title: '时间信息',
fields: [
{
label: '创建时间',
prop: 'created_at',
formatter: (value) => formatDateTime(value)
},
{
label: '支付时间',
prop: 'paid_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
},
{
label: '完成时间',
prop: 'completed_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
},
{
label: '更新时间',
prop: 'updated_at',
formatter: (value) => formatDateTime(value)
}
]
}
])
const detailSections = computed((): DetailSection[] => {
if (!detailData.value) return []
const isOfflineRecharge =
detailData.value.recharge_source === 'platform_offline' ||
detailData.value.payment_method === 'offline'
const sections: DetailSection[] = [
{
title: '订单信息',
fields: [
{ label: '充值单号', prop: 'recharge_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '充值来源', formatter: (_, data) => getRechargeSourceText(data) },
{ label: '提交人', prop: 'submitter_name', formatter: (value) => value || '-' },
{
label: '充值金额',
formatter: (_, data) => formatCurrency(data.amount)
},
{
label: '状态',
render: (data) =>
h(ElTag, { type: getStatusType(data.status) }, () => data.status_name || '-')
},
{
label: '驳回原因',
prop: 'rejection_reason',
formatter: (value) => formatRejectionReason(value),
fullWidth: true
}
]
},
...(isOfflineRecharge
? [
{
title: '企微审批信息',
fields: [
{
label: '审批渠道',
formatter: (_: unknown, data: AgentRecharge) => getApprovalProviderText(data)
},
{
label: '审批状态',
formatter: (_: unknown, data: AgentRecharge) => data.approval_status_name || '-'
},
{
label: '当前审批人摘要',
formatter: (_: unknown, data: AgentRecharge) =>
data.current_approver_summary || '-',
fullWidth: true
}
]
}
]
: []),
{
title: '业务处理结果',
fields: [
{
label: '处理状态',
formatter: (_, data) => data.processing_status_name || '-'
}
]
},
{
title: '支付信息',
fields: [
{
label: '支付方式',
formatter: (_, data) => getPaymentMethodText(data.payment_method)
},
{
label: '支付通道',
prop: 'payment_channel',
formatter: (value) => value || '-'
},
{
label: '第三方支付流水号',
prop: 'payment_transaction_id',
formatter: (value) => value || '-',
fullWidth: true
},
{
label: '支付单号',
prop: 'payment_no',
formatter: (value) => value || '-',
fullWidth: true
},
...(isOfflineRecharge
? [
{
label: '支付凭证',
fullWidth: true,
render: (data: AgentRecharge) =>
hasVoucherKeys(data.payment_voucher_key)
? h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => {
paymentVoucherFileKeys.value = toVoucherKeyList(
data.payment_voucher_key
)
}
},
() => '查看支付凭证'
)
: h('span', '-')
}
]
: []),
{
label: '运营备注',
prop: 'remark',
formatter: (value) => value || '-',
fullWidth: true
}
]
},
{
title: '时间信息',
fields: [
{
label: '创建时间',
prop: 'created_at',
formatter: (value) => formatDateTime(value)
},
{
label: '支付时间',
prop: 'paid_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
},
{
label: '完成时间',
prop: 'completed_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
},
{
label: '更新时间',
prop: 'updated_at',
formatter: (value) => formatDateTime(value)
}
]
}
]
return sections
})
// 加载详情数据
const loadDetailData = async () => {

View File

@@ -21,8 +21,8 @@
<ElButton
type="primary"
@click="showCreateDialog"
v-if="hasAuth('agent_recharge:create')"
>创建充值订单</ElButton
v-if="hasAuth('agent_recharge:create') && canCreateRecharge"
>{{ createButtonLabel }}</ElButton
>
<ElButton v-if="hasAuth('agent_recharge:export')" @click="exportDialogVisible = true">
导出
@@ -54,7 +54,7 @@
<!-- 创建充值订单对话框 -->
<ElDialog
v-model="createDialogVisible"
title="创建充值订单"
:title="createDialogTitle"
width="500px"
@closed="handleCreateDialogClosed"
>
@@ -62,14 +62,17 @@
<ElFormItem label="充值金额" prop="amount">
<ElInputNumber
v-model="createForm.amount"
:min="0.01"
:min="minimumAmountYuan"
:max="maximumAmountYuan"
:precision="2"
:step="0.01"
style="width: 100%"
placeholder="请输入充值金额(元)"
/>
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
最小: ¥0.01最大: ¥1,000,000.00
金额范围¥{{ minimumAmountYuan.toFixed(2) }} - ¥{{
maximumAmountYuan.toLocaleString('zh-CN', { minimumFractionDigits: 2 })
}}
</div>
</ElFormItem>
<ElFormItem label="支付方式" prop="payment_method">
@@ -77,11 +80,27 @@
v-model="createForm.payment_method"
placeholder="请选择支付方式"
style="width: 100%"
:loading="paymentMethodsLoading"
>
<ElOption label="线下转账" value="offline" />
<ElOption
v-for="option in availablePaymentMethodOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</ElSelect>
<div
v-if="
createMode === 'online' &&
!paymentMethodsLoading &&
onlinePaymentMethods.length === 0
"
class="online-recharge-empty-hint"
>
当前暂无可用在线支付方式,暂时无法提交充值。
</div>
</ElFormItem>
<ElFormItem label="店铺" prop="shop_id">
<ElFormItem v-if="createMode === 'offline'" label="店铺" prop="shop_id">
<ElCascader
v-model="createForm.shop_id"
:options="shopCascadeOptions"
@@ -106,7 +125,7 @@
@change="createFormRef?.validateField('payment_voucher_key')"
/>
</ElFormItem>
<ElFormItem label="运营备注" prop="remark">
<ElFormItem v-if="createMode === 'offline'" label="运营备注" prop="remark">
<ElInput
v-model="createForm.remark"
type="textarea"
@@ -123,15 +142,54 @@
<ElButton
type="primary"
@click="handleCreateRecharge"
:loading="createLoading || voucherUploading"
:disabled="voucherUploading"
:loading="createLoading || voucherUploading || paymentMethodsLoading"
:disabled="createSubmitDisabled"
>
{{ voucherUploading ? '凭证上传中...' : '确认创建' }}
{{ voucherUploading ? '凭证上传中...' : createButtonLabel }}
</ElButton>
</div>
</template>
</ElDialog>
<!-- 在线充值二维码对话框 -->
<ElDialog
v-model="onlineQrDialogVisible"
title="扫码完成充值"
width="430px"
align-center
@closed="handleQrDialogClosed"
>
<div class="online-recharge-qr-dialog">
<QrcodeVue
v-if="qrContent"
:value="qrContent"
:size="240"
level="H"
render-as="canvas"
/>
<ElEmpty v-else description="暂未获取到支付二维码" />
<div v-if="onlineQrRecharge" class="online-recharge-qr-dialog__summary">
<div>充值单号:{{ onlineQrRecharge.recharge_no || '-' }}</div>
<div>充值金额:{{ formatCurrency(onlineQrRecharge.amount) }}</div>
<div v-if="onlineWalletBalance !== null">
当前主钱包余额:{{ formatCurrency(onlineWalletBalance) }}
</div>
<ElTag :type="getStatusType(onlinePaymentStatus?.status || onlineQrRecharge.status)">
{{ onlinePaymentStatus?.status_name || onlineQrRecharge.status_name || '-' }}
</ElTag>
<div class="online-recharge-qr-dialog__hint">
{{ onlinePaymentStatusMessage }}
</div>
<div v-if="paymentStatusLoading" class="online-recharge-qr-dialog__loading">
正在查询支付状态...
</div>
</div>
</div>
<template #footer>
<ElButton @click="onlineQrDialogVisible = false">关闭</ElButton>
</template>
</ElDialog>
<!-- 导出任务对话框 -->
<ExportTaskCreateDialog
v-model="exportDialogVisible"
@@ -224,7 +282,8 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { AgentRechargeService, ShopService } from '@/api/modules'
import QrcodeVue from 'qrcode.vue'
import { AgentRechargeService, CommissionService, ShopService } from '@/api/modules'
import {
ElMessage,
ElTag,
@@ -240,7 +299,9 @@
AgentRecharge,
AgentRechargeQueryParams,
AgentRechargeStatus,
AgentRechargeOnlinePaymentMethod,
AgentRechargePaymentMethod,
AgentRechargePaymentStatusResponse,
CreateAgentRechargeRequest,
ConfirmOfflinePaymentRequest,
RejectAgentRechargeRequest
@@ -248,6 +309,7 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format'
import { normalizeApiError } from '@/utils/business/apiError'
import {
getApprovalStatusText,
getCurrentApproverSummaryText,
@@ -255,16 +317,36 @@
} from '@/utils/business/approvalSummary'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
import { buildAgentRechargeActions } from './agentRechargeActions'
import { formatRejectionReason } from './agentRechargeDisplay'
import {
amountYuanToFen,
createOnlineRechargeRequestId,
isOnlineRechargeTerminal,
shouldRetryOnlineCreate
} from './agentRechargeOnline'
defineOptions({ name: 'AgentRechargeList' })
const router = useRouter()
const { hasAuth } = useAuth()
const userStore = useUserStore()
const isAgentAccount = computed(() => Number(userStore.info.user_type) === 3)
const isPlatformAccount = computed(() => [1, 2].includes(Number(userStore.info.user_type)))
const canViewRecharge = computed(() => [1, 2, 3].includes(Number(userStore.info.user_type)))
const canCreateRecharge = computed(() => isAgentAccount.value || isPlatformAccount.value)
const createMode = ref<'online' | 'offline'>('offline')
const createDialogTitle = computed(() =>
createMode.value === 'online' ? '代理钱包在线扫码充值' : '创建平台线下代充'
)
const createButtonLabel = computed(() =>
createMode.value === 'online' ? '立即充值' : '创建充值订单'
)
const loading = ref(false)
const createLoading = ref(false)
@@ -276,13 +358,29 @@
const exportDialogVisible = ref(false)
const confirmPayDialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const onlineQrDialogVisible = ref(false)
const currentRecharge = ref<AgentRecharge | null>(null)
const onlineQrRecharge = ref<AgentRecharge | null>(null)
const onlinePaymentStatus = ref<AgentRechargePaymentStatusResponse | null>(null)
const onlineWalletBalance = ref<number | null>(null)
const onlinePaymentMethods = ref<AgentRechargeOnlinePaymentMethod[]>([])
const paymentMethodsLoading = ref(false)
const paymentStatusLoading = ref(false)
const qrContent = ref('')
const onlineRequestId = ref<string | null>(null)
const onlineCreateRetried = ref(false)
const paymentStatusTimer = ref<ReturnType<typeof setInterval> | null>(null)
const paymentMethodsBounds = reactive({
min_amount: 10000,
max_amount: 100000000
})
const paymentVoucherFileKeys = ref<string[]>([])
// 搜索表单初始值
const initialSearchState: AgentRechargeQueryParams = {
shop_id: undefined,
status: undefined,
recharge_source: undefined,
dateRange: [],
start_date: '',
end_date: ''
@@ -325,54 +423,72 @@
}
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: '店铺名称',
prop: 'shop_id',
type: 'select',
placeholder: '请选择店铺',
options: () =>
shopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
})),
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchShops(query)
}
},
{
label: '支付状态',
prop: 'status',
type: 'select',
placeholder: '请选择状态',
options: [
{ label: '待支付', value: 1 },
{ label: '已支付', value: 2 },
{ label: '已完成', value: 3 },
{ label: '已关闭', value: 4 },
{ label: '已退款', value: 5 },
{ label: '已驳回', value: 6 }
],
config: {
clearable: true
}
},
{
label: '起止时间',
prop: 'dateRange',
type: 'date',
config: {
type: 'daterange',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
}
const searchFormItems = computed<SearchFormItem[]>(() => {
const items: SearchFormItem[] = []
if (isPlatformAccount.value) {
items.push({
label: '店铺名称',
prop: 'shop_id',
type: 'select',
placeholder: '请选择店铺',
options: () =>
shopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
})),
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchShops(query)
}
})
}
]
items.push(
{
label: '充值状态',
prop: 'status',
type: 'select',
placeholder: '请选择状态',
options: [
{ label: '待支付', value: 1 },
{ label: '已支付', value: 2 },
{ label: '已完成', value: 3 },
{ label: '已关闭', value: 4 },
{ label: '已退款', value: 5 },
{ label: '已驳回', value: 6 }
],
config: { clearable: true }
},
{
label: '充值来源',
prop: 'recharge_source',
type: 'select',
placeholder: '请选择充值来源',
options: [
{ label: '代理在线自充', value: 'agent_online' },
{ label: '平台线下代充', value: 'platform_offline' }
],
config: { clearable: true }
},
{
label: '起止时间',
prop: 'dateRange',
type: 'date',
config: {
type: 'daterange',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
}
}
)
return items
})
// 分页
const pagination = reactive({
@@ -385,6 +501,7 @@
const columnOptions = [
{ label: '充值单号', prop: 'recharge_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '充值来源', prop: 'recharge_source' },
{ label: '充值金额', prop: 'amount' },
{ label: '状态', prop: 'status' },
{ label: '审批渠道', prop: 'approval_provider' },
@@ -406,8 +523,43 @@
const confirmPayFormRef = ref<FormInstance>()
const rejectFormRef = ref<FormInstance>()
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
const MIN_RECHARGE_AMOUNT = 0.01
const MAX_RECHARGE_AMOUNT = 1_000_000
const OFFLINE_MIN_RECHARGE_AMOUNT = 0.01
const OFFLINE_MAX_RECHARGE_AMOUNT = 1_000_000
const minimumAmountYuan = computed(() =>
createMode.value === 'online'
? paymentMethodsBounds.min_amount / 100
: OFFLINE_MIN_RECHARGE_AMOUNT
)
const maximumAmountYuan = computed(() =>
createMode.value === 'online'
? paymentMethodsBounds.max_amount / 100
: OFFLINE_MAX_RECHARGE_AMOUNT
)
const availablePaymentMethodOptions = computed(() => {
if (createMode.value === 'offline') return [{ label: '线下转账', value: 'offline' }]
return onlinePaymentMethods.value.map((method) => ({
label: method === 'wechat' ? '微信支付' : '支付宝',
value: method
}))
})
const createSubmitDisabled = computed(
() =>
voucherUploading.value ||
paymentMethodsLoading.value ||
(createMode.value === 'online' && onlinePaymentMethods.value.length === 0)
)
const onlinePaymentStatusMessage = computed(() => {
const status = onlinePaymentStatus.value?.status ?? onlineQrRecharge.value?.status
if (status === 1) return '请使用对应支付方式扫码完成支付'
if (status === 2) return '支付已成功,钱包正在入账,请稍候'
if (status === 3) return '充值成功,钱包已到账'
if (status === 4) return '充值订单已关闭,请重新发起充值'
if (status === 5) return '充值订单已退款'
if (status === 6) return '充值订单已驳回'
return '正在获取支付状态'
})
const createRules = computed<FormRules>(() => {
const rules: FormRules = {
@@ -419,12 +571,16 @@
callback()
return
}
if (Number(value) < MIN_RECHARGE_AMOUNT) {
callback(new Error(`充值金额最小为 ¥${MIN_RECHARGE_AMOUNT.toFixed(2)}`))
if (Number(value) < minimumAmountYuan.value) {
callback(new Error(`充值金额最小为 ¥${minimumAmountYuan.value.toFixed(2)}`))
return
}
if (Number(value) > MAX_RECHARGE_AMOUNT) {
callback(new Error(`充值金额最大为 ¥${MAX_RECHARGE_AMOUNT.toFixed(2)}`))
if (Number(value) > maximumAmountYuan.value) {
callback(
new Error(
`充值金额最大为 ¥${maximumAmountYuan.value.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`
)
)
return
}
callback()
@@ -435,9 +591,10 @@
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
shop_id: [{ required: true, message: '请选择目标店铺', trigger: 'change' }]
}
if (createForm.payment_method === 'offline') {
if (createMode.value === 'offline') {
rules.payment_voucher_key = [{ required: true, message: '请上传支付凭证', trigger: 'change' }]
}
if (createMode.value === 'online') delete rules.shop_id
return rules
})
@@ -452,13 +609,13 @@
})
const createForm = reactive<{
amount: number
payment_method: string
amount: number | null
payment_method: AgentRechargePaymentMethod | ''
shop_id: number | null
payment_voucher_key: string[]
remark: string
}>({
amount: MIN_RECHARGE_AMOUNT,
amount: OFFLINE_MIN_RECHARGE_AMOUNT,
payment_method: '',
shop_id: null,
payment_voucher_key: [],
@@ -499,11 +656,19 @@
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
const methodMap: Record<AgentRechargePaymentMethod, string> = {
wechat: '微信在线支付',
alipay: '支付宝在线支付',
offline: '线下转账'
}
return methodMap[method] || method
}
const getRechargeSourceText = (row: AgentRecharge): string => {
if (row.recharge_source_name) return row.recharge_source_name
if (row.recharge_source === 'agent_online') return '代理在线自充'
if (row.recharge_source === 'platform_offline') return '平台线下代充'
return '-'
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
@@ -530,6 +695,12 @@
minWidth: 150,
showOverflowTooltip: true
},
{
prop: 'recharge_source',
label: '充值来源',
width: 140,
formatter: (row: AgentRecharge) => getRechargeSourceText(row)
},
{
prop: 'amount',
label: '充值金额',
@@ -635,7 +806,17 @@
onMounted(() => {
getTableData()
loadShops()
if (isPlatformAccount.value) loadShops()
document.addEventListener('visibilitychange', handleDocumentVisibilityChange)
})
onBeforeUnmount(() => {
stopPaymentStatusPolling()
document.removeEventListener('visibilitychange', handleDocumentVisibilityChange)
})
onDeactivated(() => {
stopPaymentStatusPolling()
})
let isFirstActivation = true
@@ -643,6 +824,9 @@
if (!isFirstActivation) {
getTableData()
}
if (onlineQrDialogVisible.value && document.visibilityState === 'visible') {
startPaymentStatusPolling()
}
isFirstActivation = false
})
@@ -693,13 +877,20 @@
// 获取充值订单列表
const getTableData = async () => {
if (!canViewRecharge.value) {
rechargeList.value = []
pagination.total = 0
return
}
loading.value = true
try {
const params: AgentRechargeQueryParams = {
page: pagination.page,
page_size: pagination.page_size,
shop_id: searchForm.shop_id,
shop_id: isPlatformAccount.value ? searchForm.shop_id : undefined,
status: searchForm.status,
recharge_source: searchForm.recharge_source,
start_date: searchForm.start_date || undefined,
end_date: searchForm.end_date || undefined
}
@@ -737,8 +928,9 @@
}
const exportQuery = computed(() => ({
shop_id: searchForm.shop_id,
shop_id: isPlatformAccount.value ? searchForm.shop_id : undefined,
status: searchForm.status,
recharge_source: searchForm.recharge_source,
start_date: searchForm.start_date || searchForm.dateRange?.[0],
end_date: searchForm.end_date || searchForm.dateRange?.[1]
}))
@@ -761,22 +953,56 @@
// 显示创建订单对话框
const showCreateDialog = async () => {
// 重新加载店铺列表,确保获取最新数据
await loadShops()
createForm.payment_method = ''
createMode.value = isAgentAccount.value ? 'online' : 'offline'
resetCreateForm()
if (createMode.value === 'online') {
await loadPaymentMethods()
} else {
// 重新加载店铺列表,确保获取最新数据
await loadShops()
}
createDialogVisible.value = true
}
const resetCreateForm = () => {
createForm.amount = minimumAmountYuan.value
createForm.payment_method = ''
createForm.shop_id = null
createForm.payment_voucher_key = []
createForm.remark = ''
onlineRequestId.value = null
onlineCreateRetried.value = false
voucherUploading.value = false
uploadRef.value?.clearFiles(false)
}
// 对话框关闭后的清理
const handleCreateDialogClosed = () => {
createFormRef.value?.resetFields()
createForm.amount = MIN_RECHARGE_AMOUNT
createForm.payment_method = ''
createForm.shop_id = null
createForm.payment_voucher_key = []
createForm.remark = ''
voucherUploading.value = false
uploadRef.value?.clearFiles(false)
resetCreateForm()
}
// 加载代理在线充值可用支付方式
const loadPaymentMethods = async () => {
paymentMethodsLoading.value = true
onlinePaymentMethods.value = []
try {
const res = await AgentRechargeService.getPaymentMethods()
if (res.code === 0) {
onlinePaymentMethods.value = Array.isArray(res.data?.methods) ? res.data.methods : []
paymentMethodsBounds.min_amount = Number(res.data?.min_amount) || 10000
paymentMethodsBounds.max_amount = Number(res.data?.max_amount) || 100000000
createForm.amount = minimumAmountYuan.value
} else {
ElMessage.warning(res.msg || '当前暂无可用在线支付方式')
}
} catch (error) {
console.error('加载在线支付方式失败:', error)
} finally {
paymentMethodsLoading.value = false
}
}
// 创建充值订单
@@ -788,36 +1014,199 @@
return
}
await formRef.validate(async (valid) => {
if (valid) {
createLoading.value = true
try {
const data: CreateAgentRechargeRequest = {
amount: Math.round(createForm.amount * 100), // 元转分
payment_method: createForm.payment_method as AgentRechargePaymentMethod,
shop_id: createForm.shop_id!,
remark: createForm.remark || undefined
}
const valid = await formRef.validate().catch(() => false)
if (!valid || createForm.amount == null || !createForm.payment_method) return
if (
createForm.payment_method === 'offline' &&
hasVoucherKeys(createForm.payment_voucher_key)
) {
data.payment_voucher_key = toVoucherKeyList(createForm.payment_voucher_key)
}
createLoading.value = true
try {
if (createMode.value === 'online') {
await handleOnlineRechargeCreate(createForm.amount, createForm.payment_method)
return
}
await AgentRechargeService.createAgentRecharge(data)
ElMessage.success('充值订单创建成功')
createDialogVisible.value = false
formRef.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
createLoading.value = false
if (!createForm.shop_id) {
ElMessage.warning('请选择目标店铺')
return
}
const voucherKeys = toVoucherKeyList(createForm.payment_voucher_key)
if (!hasVoucherKeys(voucherKeys)) {
ElMessage.warning('请上传支付凭证')
return
}
const data: CreateAgentRechargeRequest = {
amount: amountYuanToFen(createForm.amount),
payment_method: 'offline',
shop_id: createForm.shop_id,
payment_voucher_key: voucherKeys,
remark: createForm.remark || undefined
}
const res = await AgentRechargeService.createAgentRecharge(data)
if (res.code !== 0) {
ElMessage.error(res.msg || '充值订单创建失败')
return
}
ElMessage.success('充值订单创建成功')
createDialogVisible.value = false
await getTableData()
} catch (error) {
console.error(error)
} finally {
createLoading.value = false
}
}
const handleOnlineRechargeCreate = async (
amountYuan: number,
paymentMethod: AgentRechargePaymentMethod
) => {
if (paymentMethod === 'offline') return
const requestId = onlineRequestId.value || createOnlineRechargeRequestId()
onlineRequestId.value = requestId
onlineCreateRetried.value = false
const request: CreateAgentRechargeRequest = {
amount: amountYuanToFen(amountYuan),
payment_method: paymentMethod,
request_id: requestId
}
let attempt = 0
while (attempt < 2) {
try {
const res = await AgentRechargeService.createAgentRecharge(request)
if (res.code !== 0) {
ElMessage.error(res.msg || '在线充值创建失败')
onlineRequestId.value = null
return
}
if (!res.data?.qr_content) {
ElMessage.error('支付接口未返回有效二维码,请刷新后重试')
return
}
onlineQrRecharge.value = res.data
onlinePaymentStatus.value = null
qrContent.value = res.data.qr_content
onlineQrDialogVisible.value = true
createDialogVisible.value = false
onlineRequestId.value = null
await getTableData()
return
} catch (error) {
if (attempt === 0 && shouldRetryOnlineCreate(error)) {
attempt += 1
onlineCreateRetried.value = true
continue
}
const normalized = normalizeApiError(error)
if (normalized.kind === 'validation' || normalized.kind === 'conflict') {
ElMessage.error(normalized.message)
} else {
ElMessage.warning('在线充值请求结果未知,请勿重复提交;可关闭后刷新列表确认')
}
onlineRequestId.value = null
return
}
}
}
const stopPaymentStatusPolling = () => {
if (paymentStatusTimer.value) {
clearInterval(paymentStatusTimer.value)
paymentStatusTimer.value = null
}
}
const loadOnlinePaymentStatus = async () => {
const rechargeId = onlineQrRecharge.value?.id
if (!rechargeId || !onlineQrDialogVisible.value || document.visibilityState !== 'visible')
return
paymentStatusLoading.value = true
try {
const res = await AgentRechargeService.getPaymentStatus(rechargeId)
if (res.code !== 0) return
onlinePaymentStatus.value = res.data
if (onlineQrRecharge.value) {
onlineQrRecharge.value = {
...onlineQrRecharge.value,
status: res.data.status,
status_name: res.data.status_name || onlineQrRecharge.value.status_name,
paid_at: res.data.paid_at,
completed_at: res.data.completed_at
}
}
})
if (isOnlineRechargeTerminal(res.data.status)) {
stopPaymentStatusPolling()
if (res.data.status === 3) {
await Promise.all([getTableData(), refreshAgentWalletBalance()])
}
}
} catch (error) {
console.error('查询在线充值状态失败:', error)
} finally {
paymentStatusLoading.value = false
}
}
const startPaymentStatusPolling = () => {
stopPaymentStatusPolling()
void loadOnlinePaymentStatus()
paymentStatusTimer.value = setInterval(() => {
if (!onlineQrDialogVisible.value || document.visibilityState !== 'visible') {
stopPaymentStatusPolling()
return
}
void loadOnlinePaymentStatus()
}, 3000)
}
const handleDocumentVisibilityChange = () => {
if (document.visibilityState === 'visible' && onlineQrDialogVisible.value) {
startPaymentStatusPolling()
} else if (document.visibilityState === 'hidden') {
stopPaymentStatusPolling()
}
}
watch(onlineQrDialogVisible, (visible) => {
if (visible) {
startPaymentStatusPolling()
} else {
stopPaymentStatusPolling()
}
})
const handleQrDialogClosed = () => {
stopPaymentStatusPolling()
qrContent.value = ''
onlineQrRecharge.value = null
onlinePaymentStatus.value = null
onlineWalletBalance.value = null
paymentStatusLoading.value = false
}
const refreshAgentWalletBalance = async () => {
if (!isAgentAccount.value) return
try {
const res = await CommissionService.getShopFundSummary({ page: 1, page_size: 100 })
const currentShopId = Number(userStore.info.shop_id)
const currentShop =
res.code === 0 ? res.data.items?.find((item) => item.shop_id === currentShopId) : null
if (currentShop) onlineWalletBalance.value = currentShop.main_balance
} catch (error) {
console.error('刷新代理钱包余额失败:', error)
}
}
// 显示确认支付对话框
@@ -935,4 +1324,38 @@
.agent-recharge-page {
height: 100%;
}
.online-recharge-empty-hint {
margin-top: 8px;
color: var(--el-color-danger);
font-size: 12px;
line-height: 1.5;
}
.online-recharge-qr-dialog {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
&__summary {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100%;
color: var(--el-text-color-regular);
font-size: 14px;
text-align: center;
}
&__hint {
color: var(--el-text-color-secondary);
}
&__loading {
color: var(--el-color-primary);
font-size: 12px;
}
}
</style>