增加修改客户重新同步功能
This commit is contained in:
@@ -29,3 +29,19 @@ export function retryBillingTaskBatch(taskIds) {
|
||||
data: taskIds
|
||||
})
|
||||
}
|
||||
|
||||
/** Async retry by customerCode + billTime range */
|
||||
export function retryBillingTaskByFilter(data) {
|
||||
return request({
|
||||
url: '/monitor/billingTask/retryByFilter',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getBillingTaskRetryProgress(asyncTaskId) {
|
||||
return request({
|
||||
url: '/monitor/billingTask/retryTask/' + asyncTaskId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<el-drawer v-model="open" size="600px" :title="drawerTitle" append-to-body destroy-on-close class="billing-task-drawer">
|
||||
<div v-loading="loading" class="detail-wrap">
|
||||
<el-empty v-if="!detail" description="暂无详情" />
|
||||
<template v-else>
|
||||
<div class="detail-header">
|
||||
<el-tag :type="statusTag(detail.successStatus)" effect="light">{{ statusLabel(detail.successStatus) }}</el-tag>
|
||||
<span class="detail-header-type">{{ billTypeLabel(detail.billType) }}</span>
|
||||
<span class="detail-header-time">计费时间:{{ detail.billTime || '-' }}</span>
|
||||
</div>
|
||||
<el-descriptions :column="1" border size="small" class="detail-desc">
|
||||
<el-descriptions-item label="任务ID">{{ detail.taskId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户代码">{{ detail.customerCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单号">{{ detail.orderNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="运单号">{{ detail.trackingNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="转单号">{{ detail.transferNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="计费订单ID">{{ detail.billId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="失败码">{{ detail.failCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ detail.createTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ detail.updateTime || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div v-if="detail.errorMessage" class="err-block">
|
||||
<div class="err-block-head">
|
||||
<span class="err-block-title">失败原因</span>
|
||||
<el-button link type="primary" size="small" icon="CopyDocument" @click="copyError">复制</el-button>
|
||||
</div>
|
||||
<div class="err-block-body">{{ detail.errorMessage }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getBillingTask } from '@/api/monitor/billingTask'
|
||||
import { billTypeLabel, statusLabel, statusTag } from './billingTaskOptions'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const open = ref(false)
|
||||
const loading = ref(false)
|
||||
const detail = ref(null)
|
||||
const drawerTitle = ref('计费任务详情')
|
||||
|
||||
function openDrawer(row) {
|
||||
if (!row || !row.taskId) return
|
||||
drawerTitle.value = '计费任务 #' + row.taskId
|
||||
open.value = true
|
||||
loading.value = true
|
||||
detail.value = null
|
||||
getBillingTask(row.taskId).then(res => {
|
||||
detail.value = res.data || row
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function copyError() {
|
||||
const text = detail.value?.errorMessage || ''
|
||||
if (!text) return
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
proxy.$modal.msgSuccess('已复制')
|
||||
}).catch(() => {
|
||||
proxy.$modal.msgError('复制失败')
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open: openDrawer })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-wrap {
|
||||
padding: 0 4px;
|
||||
}
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #f8f9fb;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.detail-header-type {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.detail-header-time {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.detail-desc :deep(.el-descriptions__label) {
|
||||
width: 96px;
|
||||
min-width: 96px;
|
||||
color: #909399;
|
||||
}
|
||||
.detail-desc :deep(.el-descriptions__content) {
|
||||
word-break: break-all;
|
||||
}
|
||||
.err-block {
|
||||
margin-top: 16px;
|
||||
border: 1px solid #fde2e2;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.err-block-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
background: #fef0f0;
|
||||
}
|
||||
.err-block-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #f56c6c;
|
||||
}
|
||||
.err-block-body {
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
line-height: 1.7;
|
||||
color: #c45656;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
width="480px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:close-on-click-modal="!loading"
|
||||
:close-on-press-escape="!loading"
|
||||
:show-close="!loading"
|
||||
@closed="onClosed"
|
||||
>
|
||||
<el-form v-show="!loading && !finished" label-width="96px" @submit.prevent>
|
||||
<el-form-item label="客户代码" required>
|
||||
<customer-select
|
||||
v-model="form.customerCode"
|
||||
value-key="customerCode"
|
||||
placeholder="请选择客户"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="计费时间" required>
|
||||
<el-date-picker
|
||||
v-model="form.dateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.successStatus" clearable placeholder="全部" style="width: 100%">
|
||||
<el-option v-for="opt in statusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="按客户+计费时间分批查询任务ID并异步重试,避免一次性加载全量数据导致OOM。"
|
||||
/>
|
||||
</el-form>
|
||||
|
||||
<div v-if="loading || finished" class="filter-retry-progress">
|
||||
<el-progress
|
||||
:percentage="progressPercent"
|
||||
:status="progressStatus"
|
||||
:stroke-width="14"
|
||||
striped
|
||||
:striped-flow="loading"
|
||||
/>
|
||||
<div class="filter-retry-progress__msg">{{ progressMessage }}</div>
|
||||
<div class="filter-retry-progress__text">
|
||||
总数 {{ progress.total || 0 }}
|
||||
/ 已处理 {{ progress.processed || 0 }}
|
||||
/ 成功 {{ progress.ok || 0 }}
|
||||
/ 失败 {{ progress.fail || 0 }}
|
||||
</div>
|
||||
<div v-if="progress.error" class="filter-retry-progress__error">{{ progress.error }}</div>
|
||||
<p v-if="loading" class="filter-retry-progress__hint">请勿关闭页面,大量任务重新计费可能需要一些时间</p>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<template v-if="!loading && !finished">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submit">开始重新同步</el-button>
|
||||
</template>
|
||||
<template v-else-if="finished">
|
||||
<el-button type="primary" @click="visible = false">关闭</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button disabled>执行中...</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { retryBillingTaskByFilter, getBillingTaskRetryProgress } from '@/api/monitor/billingTask'
|
||||
import { statusOptions } from './billingTaskOptions'
|
||||
|
||||
const emit = defineEmits(['success', 'progress'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const progress = ref({
|
||||
percent: 0,
|
||||
message: '',
|
||||
total: 0,
|
||||
processed: 0,
|
||||
ok: 0,
|
||||
fail: 0,
|
||||
status: '',
|
||||
error: ''
|
||||
})
|
||||
const form = reactive({
|
||||
customerCode: '',
|
||||
dateRange: [],
|
||||
successStatus: 'ALL'
|
||||
})
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (loading.value) return '异步重新计费进行中'
|
||||
if (finished.value) return '异步重新计费完成'
|
||||
return '条件批量重新同步'
|
||||
})
|
||||
|
||||
const progressPercent = computed(() => Math.min(100, Math.max(0, Number(progress.value?.percent) || 0)))
|
||||
|
||||
const progressStatus = computed(() => {
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS') return 'success'
|
||||
if (st === 'FAIL') return 'exception'
|
||||
return undefined
|
||||
})
|
||||
|
||||
const progressMessage = computed(() => mapProgressMessage(progress.value?.message, progress.value))
|
||||
|
||||
function mapProgressMessage(message, data) {
|
||||
const m = message || ''
|
||||
if (!m || m === 'queued') return '排队中...'
|
||||
if (m === 'starting') return '正在统计任务...'
|
||||
if (m === 'no tasks') return '无匹配任务'
|
||||
if (m === 'failed') return '执行失败'
|
||||
if (m.startsWith('processing ')) {
|
||||
return '正在重新计费 ' + m.slice('processing '.length)
|
||||
}
|
||||
if (m.startsWith('done ')) {
|
||||
return '完成:成功 ' + (data?.ok || 0) + ' / 失败 ' + (data?.fail || 0)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
function emitProgress() {
|
||||
emit('progress', {
|
||||
loading: loading.value,
|
||||
finished: finished.value,
|
||||
percent: progressPercent.value,
|
||||
status: progress.value?.status || '',
|
||||
message: progressMessage.value,
|
||||
total: progress.value?.total || 0,
|
||||
processed: progress.value?.processed || 0,
|
||||
ok: progress.value?.ok || 0,
|
||||
fail: progress.value?.fail || 0,
|
||||
error: progress.value?.error || ''
|
||||
})
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (loading.value || finished.value) {
|
||||
visible.value = true
|
||||
return
|
||||
}
|
||||
form.customerCode = ''
|
||||
form.dateRange = []
|
||||
form.successStatus = 'ALL'
|
||||
progress.value = {
|
||||
percent: 0,
|
||||
message: '',
|
||||
total: 0,
|
||||
processed: 0,
|
||||
ok: 0,
|
||||
fail: 0,
|
||||
status: '',
|
||||
error: ''
|
||||
}
|
||||
loading.value = false
|
||||
finished.value = false
|
||||
visible.value = true
|
||||
emitProgress()
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
if (!loading.value) {
|
||||
finished.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const customerCode = (form.customerCode || '').trim()
|
||||
const range = form.dateRange || []
|
||||
if (!customerCode) {
|
||||
proxy.$modal.msgError('请选择客户')
|
||||
return
|
||||
}
|
||||
if (!range.length || range.length < 2) {
|
||||
proxy.$modal.msgError('请选择计费时间范围')
|
||||
return
|
||||
}
|
||||
proxy.$modal.confirm(
|
||||
'确认按客户 ' + customerCode + ' 、计费时间 ' + range[0] + ' ~ ' + range[1] + ' 异步批量重新计费?'
|
||||
).then(() => {
|
||||
loading.value = true
|
||||
finished.value = false
|
||||
progress.value = {
|
||||
percent: 0,
|
||||
message: 'queued',
|
||||
total: 0,
|
||||
processed: 0,
|
||||
ok: 0,
|
||||
fail: 0,
|
||||
status: 'PENDING',
|
||||
error: ''
|
||||
}
|
||||
emitProgress()
|
||||
return retryBillingTaskByFilter({
|
||||
customerCode,
|
||||
beginTime: range[0],
|
||||
endTime: range[1],
|
||||
successStatus: form.successStatus || 'ALL',
|
||||
batchSize: 50
|
||||
})
|
||||
}).then(res => {
|
||||
const asyncTaskId = res.data?.asyncTaskId || res.data
|
||||
if (!asyncTaskId) {
|
||||
throw new Error('asyncTaskId missing')
|
||||
}
|
||||
return poll(asyncTaskId)
|
||||
}).then((data) => {
|
||||
finished.value = true
|
||||
emit('success', data)
|
||||
proxy.$modal.msgSuccess(
|
||||
'完成:成功 ' + (data.ok || 0) + ' / 失败 ' + (data.fail || 0)
|
||||
)
|
||||
}).catch(() => {
|
||||
if (loading.value) {
|
||||
finished.value = true
|
||||
if (!progress.value?.status || progress.value.status === 'PENDING' || progress.value.status === 'RUNNING') {
|
||||
progress.value = {
|
||||
...progress.value,
|
||||
status: 'FAIL',
|
||||
percent: 100,
|
||||
message: 'failed',
|
||||
error: progress.value?.error || '批量重新计费失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
emitProgress()
|
||||
})
|
||||
}
|
||||
|
||||
function poll(asyncTaskId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tick = () => {
|
||||
getBillingTaskRetryProgress(asyncTaskId).then(res => {
|
||||
const data = res.data || {}
|
||||
progress.value = {
|
||||
percent: Number(data.percent) || 0,
|
||||
message: data.message || '',
|
||||
total: data.total || 0,
|
||||
processed: data.processed || 0,
|
||||
ok: data.ok || 0,
|
||||
fail: data.fail || 0,
|
||||
status: data.status || '',
|
||||
error: data.error || ''
|
||||
}
|
||||
emitProgress()
|
||||
const st = (data.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS') {
|
||||
progress.value.percent = 100
|
||||
resolve(data)
|
||||
return
|
||||
}
|
||||
if (st === 'FAIL') {
|
||||
proxy.$modal.msgError(data.error || data.message || '批量重新计费失败')
|
||||
reject(new Error(data.error || 'fail'))
|
||||
return
|
||||
}
|
||||
setTimeout(tick, 1500)
|
||||
}).catch(err => reject(err))
|
||||
}
|
||||
tick()
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-retry-progress {
|
||||
padding: 8px 4px 0;
|
||||
}
|
||||
.filter-retry-progress__msg {
|
||||
margin-top: 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.filter-retry-progress__text {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.filter-retry-progress__error {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #f56c6c;
|
||||
word-break: break-all;
|
||||
}
|
||||
.filter-retry-progress__hint {
|
||||
margin: 12px 0 0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
/** Shared labels/options for billing task monitor */
|
||||
export const statusOptions = [
|
||||
{ label: '失败', value: 'FAILED' },
|
||||
{ label: '待处理', value: 'WAITING' },
|
||||
{ label: '成功', value: 'SUCCESS' },
|
||||
{ label: '全部', value: 'ALL' }
|
||||
]
|
||||
|
||||
export const billTypeOptions = [
|
||||
{ label: '待计费', value: 'PENDING' },
|
||||
{ label: '重计费', value: 'REBILL' },
|
||||
{ label: '退费', value: 'REFUND' }
|
||||
]
|
||||
|
||||
export const failCodeOptions = [
|
||||
{ label: 'NO_CUSTOMER_PACKAGE', value: 'NO_CUSTOMER_PACKAGE' },
|
||||
{ label: 'NO_ACTIVE_PACKAGE_VERSION', value: 'NO_ACTIVE_PACKAGE_VERSION' },
|
||||
{ label: 'ALREADY_IN_AR_BILL', value: 'ALREADY_IN_AR_BILL' },
|
||||
{ label: 'PRODUCT_ARCHIVE_MISSING', value: 'PRODUCT_ARCHIVE_MISSING' },
|
||||
{ label: 'PRICE_NOT_HIT', value: 'PRICE_NOT_HIT' },
|
||||
{ label: 'ZONE_PRICE_NOT_HIT', value: 'ZONE_PRICE_NOT_HIT' },
|
||||
{ label: 'POSTCODE_REQUIRED', value: 'POSTCODE_REQUIRED' },
|
||||
{ label: 'POSTCODE_ZONE_NOT_FOUND', value: 'POSTCODE_ZONE_NOT_FOUND' },
|
||||
{ label: 'ORACLE_ORDER_NOT_FOUND', value: 'ORACLE_ORDER_NOT_FOUND' },
|
||||
{ label: 'ORACLE_ORDERNO_EMPTY', value: 'ORACLE_ORDERNO_EMPTY' },
|
||||
{ label: 'ORACLE_CUSTOMER_EMPTY', value: 'ORACLE_CUSTOMER_EMPTY' },
|
||||
{ label: 'ORACLE_PRODUCT_EMPTY', value: 'ORACLE_PRODUCT_EMPTY' },
|
||||
{ label: 'ORACLE_WEIGHT_INVALID', value: 'ORACLE_WEIGHT_INVALID' },
|
||||
{ label: 'BILL_TIME_EMPTY', value: 'BILL_TIME_EMPTY' },
|
||||
{ label: 'REFUND_ORDER_UNRESOLVED', value: 'REFUND_ORDER_UNRESOLVED' },
|
||||
{ label: 'CUSTOMER_CODE_REQUIRED', value: 'CUSTOMER_CODE_REQUIRED' },
|
||||
{ label: 'CUSTOMER_DISABLED', value: 'CUSTOMER_DISABLED' },
|
||||
{ label: 'FREIGHT_MISSING', value: 'FREIGHT_MISSING' },
|
||||
{ label: 'UNKNOWN', value: 'UNKNOWN' }
|
||||
]
|
||||
|
||||
export function billTypeLabel(type) {
|
||||
const found = billTypeOptions.find(item => item.value === type)
|
||||
return found ? found.label : (type || '-')
|
||||
}
|
||||
|
||||
export function statusLabel(status) {
|
||||
const found = statusOptions.find(item => item.value === status)
|
||||
return found ? found.label : (status || '-')
|
||||
}
|
||||
|
||||
export function statusTag(status) {
|
||||
if (status === 'SUCCESS') return 'success'
|
||||
if (status === 'FAILED') return 'danger'
|
||||
if (status === 'WAITING') return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
@@ -1,5 +1,19 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="app-container billing-task-page">
|
||||
<div v-if="retryFloatVisible" class="retry-status-float">
|
||||
<el-tooltip :content="retryFloatTooltip" placement="bottom">
|
||||
<el-button class="retry-status-button" type="primary" plain size="small" @click="filterRetryRef.open()">
|
||||
<span class="retry-progress-ring" :style="{ '--retry-progress': retryProgressDegree + 'deg' }" aria-hidden="true">
|
||||
<span class="retry-progress-value">{{ retryProgressText }}</span>
|
||||
</span>
|
||||
<span class="retry-status-text">
|
||||
<strong>{{ retryFloatTitle }}</strong>
|
||||
<small>{{ retryFloatSummary }}</small>
|
||||
</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
@@ -15,11 +29,20 @@
|
||||
type="primary"
|
||||
plain
|
||||
icon="Refresh"
|
||||
v-hasPermi="['monitor:billingTask:retry']"
|
||||
@click="filterRetryRef.open()"
|
||||
>批量重新同步</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="Refresh"
|
||||
:disabled="!ids.length"
|
||||
:loading="batchLoading"
|
||||
v-hasPermi="['monitor:billingTask:retry']"
|
||||
@click="handleBatchRetry(ids)"
|
||||
>批量重新同步</el-button>
|
||||
>同步选中</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
<template #col-billType="{ row }">
|
||||
@@ -32,7 +55,7 @@
|
||||
<span class="err-text" :title="row.errorMessage || ''">{{ row.errorMessage || '-' }}</span>
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleView(row)" v-hasPermi="['monitor:billingTask:list']">查看</el-button>
|
||||
<el-button link type="primary" icon="View" @click="detailRef.open(row)" v-hasPermi="['monitor:billingTask:list']">查看</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
@@ -44,87 +67,38 @@
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
<el-drawer v-model="open" size="600px" :title="drawerTitle" append-to-body destroy-on-close class="billing-task-drawer">
|
||||
<div v-loading="loading" class="detail-wrap">
|
||||
<el-empty v-if="!detail" description="暂无详情" />
|
||||
<template v-else>
|
||||
<div class="detail-header">
|
||||
<el-tag :type="statusTag(detail.successStatus)" effect="light">{{ statusLabel(detail.successStatus) }}</el-tag>
|
||||
<span class="detail-header-type">{{ billTypeLabel(detail.billType) }}</span>
|
||||
<span class="detail-header-time">计费时间:{{ detail.billTime || '-' }}</span>
|
||||
</div>
|
||||
<el-descriptions :column="1" border size="small" class="detail-desc">
|
||||
<el-descriptions-item label="任务ID">{{ detail.taskId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户代码">{{ detail.customerCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单号">{{ detail.orderNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="运单号">{{ detail.trackingNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="转单号">{{ detail.transferNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="计费订单ID">{{ detail.billId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="失败码">{{ detail.failCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ detail.createTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ detail.updateTime || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div v-if="detail.errorMessage" class="err-block">
|
||||
<div class="err-block-head">
|
||||
<span class="err-block-title">失败原因</span>
|
||||
<el-button link type="primary" size="small" icon="CopyDocument" @click="copyError">复制</el-button>
|
||||
</div>
|
||||
<div class="err-block-body">{{ detail.errorMessage }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-drawer>
|
||||
<billing-task-detail-drawer ref="detailRef" />
|
||||
<billing-task-filter-retry-dialog
|
||||
ref="filterRetryRef"
|
||||
@progress="onFilterRetryProgress"
|
||||
@success="onFilterRetrySuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="BillingTaskMonitor">
|
||||
import { listBillingTask, getBillingTask, retryBillingTask, retryBillingTaskBatch } from '@/api/monitor/billingTask'
|
||||
import { listBillingTask, retryBillingTask, retryBillingTaskBatch } from '@/api/monitor/billingTask'
|
||||
import BillingTaskDetailDrawer from './components/BillingTaskDetailDrawer.vue'
|
||||
import BillingTaskFilterRetryDialog from './components/BillingTaskFilterRetryDialog.vue'
|
||||
import {
|
||||
statusOptions,
|
||||
billTypeOptions,
|
||||
failCodeOptions,
|
||||
billTypeLabel,
|
||||
statusLabel,
|
||||
statusTag
|
||||
} from './components/billingTaskOptions'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const open = ref(false)
|
||||
const loading = ref(false)
|
||||
const detail = ref(null)
|
||||
const drawerTitle = ref('计费任务详情')
|
||||
const detailRef = ref()
|
||||
const filterRetryRef = ref()
|
||||
const retryingId = ref(null)
|
||||
const batchLoading = ref(false)
|
||||
const retryFloat = ref(null)
|
||||
|
||||
const defaultParams = { successStatus: 'FAILED' }
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '失败', value: 'FAILED' },
|
||||
{ label: '待处理', value: 'WAITING' },
|
||||
{ label: '成功', value: 'SUCCESS' },
|
||||
{ label: '全部', value: 'ALL' }
|
||||
]
|
||||
|
||||
const failCodeOptions = [
|
||||
{ label: 'NO_CUSTOMER_PACKAGE', value: 'NO_CUSTOMER_PACKAGE' },
|
||||
{ label: 'NO_ACTIVE_PACKAGE_VERSION', value: 'NO_ACTIVE_PACKAGE_VERSION' },
|
||||
{ label: 'ALREADY_IN_AR_BILL', value: 'ALREADY_IN_AR_BILL' },
|
||||
{ label: 'PRODUCT_ARCHIVE_MISSING', value: 'PRODUCT_ARCHIVE_MISSING' },
|
||||
{ label: 'PRICE_NOT_HIT', value: 'PRICE_NOT_HIT' },
|
||||
{ label: 'ZONE_PRICE_NOT_HIT', value: 'ZONE_PRICE_NOT_HIT' },
|
||||
{ label: 'POSTCODE_REQUIRED', value: 'POSTCODE_REQUIRED' },
|
||||
{ label: 'POSTCODE_ZONE_NOT_FOUND', value: 'POSTCODE_ZONE_NOT_FOUND' },
|
||||
{ label: 'ORACLE_ORDER_NOT_FOUND', value: 'ORACLE_ORDER_NOT_FOUND' },
|
||||
{ label: 'ORACLE_ORDERNO_EMPTY', value: 'ORACLE_ORDERNO_EMPTY' },
|
||||
{ label: 'ORACLE_CUSTOMER_EMPTY', value: 'ORACLE_CUSTOMER_EMPTY' },
|
||||
{ label: 'ORACLE_PRODUCT_EMPTY', value: 'ORACLE_PRODUCT_EMPTY' },
|
||||
{ label: 'ORACLE_WEIGHT_INVALID', value: 'ORACLE_WEIGHT_INVALID' },
|
||||
{ label: 'BILL_TIME_EMPTY', value: 'BILL_TIME_EMPTY' },
|
||||
{ label: 'REFUND_ORDER_UNRESOLVED', value: 'REFUND_ORDER_UNRESOLVED' },
|
||||
{ label: 'CUSTOMER_CODE_REQUIRED', value: 'CUSTOMER_CODE_REQUIRED' },
|
||||
{ label: 'CUSTOMER_DISABLED', value: 'CUSTOMER_DISABLED' },
|
||||
{ label: 'FREIGHT_MISSING', value: 'FREIGHT_MISSING' },
|
||||
{ label: 'UNKNOWN', value: 'UNKNOWN' }
|
||||
]
|
||||
const billTypeOptions = [
|
||||
{ label: '待计费', value: 'PENDING' },
|
||||
{ label: '重计费', value: 'REBILL' },
|
||||
{ label: '退费', value: 'REFUND' }
|
||||
]
|
||||
|
||||
const searchColumns = [
|
||||
{ label: '状态', prop: 'successStatus', component: 'select', options: statusOptions, placeholder: '请选择状态', width: '140px', props: { clearable: true } },
|
||||
{ label: '计费类型', prop: 'billType', component: 'select', options: billTypeOptions, placeholder: '请选择类型', width: '140px', props: { clearable: true } },
|
||||
@@ -151,39 +125,44 @@ const columns = [
|
||||
{ label: '操作', prop: 'action', align: 'center', width: 180, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
|
||||
const retryFloatVisible = computed(() => {
|
||||
const p = retryFloat.value
|
||||
return !!(p && (p.loading || p.finished))
|
||||
})
|
||||
const retryProgressDegree = computed(() => Math.round(((retryFloat.value?.percent || 0) / 100) * 360))
|
||||
const retryProgressText = computed(() => (retryFloat.value?.percent || 0) + '%')
|
||||
const retryFloatTitle = computed(() => {
|
||||
const st = (retryFloat.value?.status || '').toUpperCase()
|
||||
if (retryFloat.value?.loading) return '异步重新计费中'
|
||||
if (st === 'SUCCESS') return '重新计费完成'
|
||||
if (st === 'FAIL') return '重新计费失败'
|
||||
return '异步重新计费'
|
||||
})
|
||||
const retryFloatSummary = computed(() => {
|
||||
const p = retryFloat.value || {}
|
||||
return '成功 ' + (p.ok || 0) + ' / 失败 ' + (p.fail || 0) + ' / 总数 ' + (p.total || 0)
|
||||
})
|
||||
const retryFloatTooltip = computed(() => retryFloat.value?.message || '点击查看详情')
|
||||
|
||||
function queryList(params) {
|
||||
const p = proxy.addDateRange({ ...params }, params.dateRange)
|
||||
delete p.dateRange
|
||||
return listBillingTask(p)
|
||||
}
|
||||
|
||||
function billTypeLabel(type) {
|
||||
const found = billTypeOptions.find(item => item.value === type)
|
||||
return found ? found.label : (type || '-')
|
||||
function onFilterRetryProgress(payload) {
|
||||
retryFloat.value = payload
|
||||
if (payload && payload.finished && !payload.loading) {
|
||||
window.setTimeout(() => {
|
||||
if (retryFloat.value && retryFloat.value.finished && !retryFloat.value.loading) {
|
||||
retryFloat.value = null
|
||||
}
|
||||
}, 8000)
|
||||
}
|
||||
}
|
||||
|
||||
function statusLabel(status) {
|
||||
const found = statusOptions.find(item => item.value === status)
|
||||
return found ? found.label : (status || '-')
|
||||
}
|
||||
|
||||
function statusTag(status) {
|
||||
if (status === 'SUCCESS') return 'success'
|
||||
if (status === 'FAILED') return 'danger'
|
||||
if (status === 'WAITING') return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function handleView(row) {
|
||||
drawerTitle.value = '计费任务 #' + row.taskId
|
||||
open.value = true
|
||||
loading.value = true
|
||||
detail.value = null
|
||||
getBillingTask(row.taskId).then(res => {
|
||||
detail.value = res.data || row
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
function onFilterRetrySuccess() {
|
||||
tableRef.value?.refresh()
|
||||
}
|
||||
|
||||
function handleRetry(row) {
|
||||
@@ -203,19 +182,9 @@ function handleRetry(row) {
|
||||
})
|
||||
}
|
||||
|
||||
function copyError() {
|
||||
const text = detail.value?.errorMessage || ''
|
||||
if (!text) return
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
proxy.$modal.msgSuccess('已复制')
|
||||
}).catch(() => {
|
||||
proxy.$modal.msgError('复制失败')
|
||||
})
|
||||
}
|
||||
|
||||
function handleBatchRetry(ids) {
|
||||
if (!ids || !ids.length) return
|
||||
proxy.$modal.confirm('确认批量重新同步选中的 ' + ids.length + ' 条任务?').then(() => {
|
||||
proxy.$modal.confirm('确认同步选中的 ' + ids.length + ' 条任务?').then(() => {
|
||||
batchLoading.value = true
|
||||
return retryBillingTaskBatch(ids)
|
||||
}).then(res => {
|
||||
@@ -234,73 +203,71 @@ function handleBatchRetry(ids) {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-wrap {
|
||||
padding: 0 4px;
|
||||
.billing-task-page .retry-status-float {
|
||||
position: fixed;
|
||||
top: 92px;
|
||||
right: 24px;
|
||||
z-index: 3001;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
.billing-task-page .retry-status-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #f8f9fb;
|
||||
border-radius: 6px;
|
||||
gap: 10px;
|
||||
min-width: 200px;
|
||||
min-height: 68px;
|
||||
padding: 6px 16px 6px 7px;
|
||||
border-radius: 36px;
|
||||
}
|
||||
|
||||
.detail-header-type {
|
||||
.billing-task-page .retry-progress-ring {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 54px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(
|
||||
var(--el-color-primary) 0deg var(--retry-progress, 45deg),
|
||||
var(--el-color-primary-light-8) var(--retry-progress, 45deg) 360deg
|
||||
);
|
||||
}
|
||||
|
||||
.billing-task-page .retry-progress-ring::after {
|
||||
position: absolute;
|
||||
inset: 5px;
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.billing-task-page .retry-progress-value {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.billing-task-page .retry-status-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.billing-task-page .retry-status-text strong {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.detail-header-time {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.detail-desc :deep(.el-descriptions__label) {
|
||||
width: 96px;
|
||||
min-width: 96px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.detail-desc :deep(.el-descriptions__content) {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.err-block {
|
||||
margin-top: 16px;
|
||||
border: 1px solid #fde2e2;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.err-block-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
background: #fef0f0;
|
||||
}
|
||||
|
||||
.err-block-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.err-block-body {
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
line-height: 1.7;
|
||||
color: #c45656;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
background: #fff;
|
||||
.billing-task-page .retry-status-text small {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user