feat(media-supply): resource cache sync and order flow refinements
Add a resource cache sync entrypoint (RunMediaResourceSyncOnce) driven by the new scheduler job, and rework the backlink/sync worker around it. Tune the meijiequan client and slow the default backlink sync interval from 10m to 30m to reduce upstream pressure. Trim unused public/transport surface (handler + router + swagger) and simplify admin-web order management and ops-web media-supply views, dropping stale shared-types fields.
This commit is contained in:
@@ -128,7 +128,6 @@ import type {
|
||||
ScheduleTaskListResponse,
|
||||
ScheduleTaskRequest,
|
||||
ScheduleTaskStatusRequest,
|
||||
SyncMediaSupplyBacklinksResponse,
|
||||
TemplateAnalyzeTaskRequest,
|
||||
TemplateAnalyzeTaskResultResponse,
|
||||
TemplateAssistTaskCreateResponse,
|
||||
@@ -1257,12 +1256,6 @@ export const mediaSupplyApi = {
|
||||
getOrder(id: number) {
|
||||
return apiClient.get<MediaSupplyOrderDetail>(`/api/tenant/media-supply/orders/${id}`)
|
||||
},
|
||||
syncBacklinks() {
|
||||
return apiClient.post<SyncMediaSupplyBacklinksResponse, Record<string, never>>(
|
||||
'/api/tenant/media-supply/orders/sync-backlinks',
|
||||
{},
|
||||
)
|
||||
},
|
||||
wallet() {
|
||||
return apiClient.get<MediaSupplyWalletStatus>('/api/tenant/media-supply/wallet')
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { CopyOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import type { MediaSupplyOrderDetail } from '@geo/shared-types'
|
||||
import { useMutation, useQuery } from '@tanstack/vue-query'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { message, type TableColumnsType } from 'ant-design-vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import { mediaSupplyApi } from '@/lib/api'
|
||||
import { formatDateTime } from '@/lib/display'
|
||||
@@ -19,11 +19,14 @@ function copyToClipboard(text?: string | null): void {
|
||||
message.warning('链接为空,无法复制')
|
||||
return
|
||||
}
|
||||
void navigator.clipboard.writeText(url).then(() => {
|
||||
message.success('已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
message.error('复制失败,请手动复制')
|
||||
})
|
||||
void navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => {
|
||||
message.success('已复制到剪贴板')
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('复制失败,请手动复制')
|
||||
})
|
||||
}
|
||||
|
||||
const ordersQuery = useQuery({
|
||||
@@ -36,35 +39,6 @@ const ordersQuery = useQuery({
|
||||
}),
|
||||
})
|
||||
|
||||
const syncBacklinksMutation = useMutation({
|
||||
mutationFn: () => mediaSupplyApi.syncBacklinks(),
|
||||
onSuccess: async (result) => {
|
||||
await ordersQuery.refetch()
|
||||
if (result.refunded_orders > 0) {
|
||||
message.warning(`已同步 ${result.refunded_orders} 个退稿并退款`)
|
||||
return
|
||||
}
|
||||
if (result.links_updated > 0) {
|
||||
message.success(`已同步 ${result.links_updated} 条回链`)
|
||||
return
|
||||
}
|
||||
if (result.order_codes_updated > 0) {
|
||||
message.success(`已同步 ${result.order_codes_updated} 个外部单号`)
|
||||
return
|
||||
}
|
||||
message.info(
|
||||
result.published_fetched > 0
|
||||
? '已检查已发表记录,暂未发现新的回链'
|
||||
: result.problem_fetched > 0
|
||||
? '已检查退稿记录,暂未发现需要退款的订单'
|
||||
: result.pending_fetched > 0
|
||||
? '已检查发表中记录,暂未发现新的外部单号'
|
||||
: '暂未读取到投稿记录',
|
||||
)
|
||||
},
|
||||
onError: () => message.error('同步回链失败,请稍后重试'),
|
||||
})
|
||||
|
||||
const orders = computed(() => ordersQuery.data.value?.items ?? [])
|
||||
const total = computed(() => ordersQuery.data.value?.total ?? 0)
|
||||
|
||||
@@ -90,10 +64,6 @@ function refresh(): void {
|
||||
void ordersQuery.refetch()
|
||||
}
|
||||
|
||||
function syncBacklinks(): void {
|
||||
void syncBacklinksMutation.mutateAsync()
|
||||
}
|
||||
|
||||
function handleTableChange(nextPage: number, nextPageSize: number): void {
|
||||
page.value = nextPage
|
||||
pageSize.value = nextPageSize
|
||||
@@ -119,7 +89,7 @@ function orderStatusMeta(order: MediaSupplyOrderDetail): { label: string; color:
|
||||
? { label: '已发表', color: 'success' }
|
||||
: { label: '发表中', color: 'processing' }
|
||||
case 'failed':
|
||||
return order.wallet_refunded_at
|
||||
return order.wallet_refunded_at && order.supplier_status === 'rejected'
|
||||
? { label: '已退稿', color: 'default' }
|
||||
: { label: '投稿失败', color: 'error' }
|
||||
default:
|
||||
@@ -149,6 +119,52 @@ function pendingBacklinkCount(order: MediaSupplyOrderDetail): number {
|
||||
return (order.items ?? []).filter((item) => !item.external_article_url).length
|
||||
}
|
||||
|
||||
const shouldPollOrders = computed(() =>
|
||||
orders.value.some(
|
||||
(order) =>
|
||||
order.status === 'queued' ||
|
||||
order.status === 'submitting' ||
|
||||
(order.status === 'submitted' && pendingBacklinkCount(order) > 0),
|
||||
),
|
||||
)
|
||||
|
||||
let pollingTimer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
function stopOrderPolling(): void {
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer)
|
||||
pollingTimer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function startOrderPolling(): void {
|
||||
if (pollingTimer) {
|
||||
return
|
||||
}
|
||||
pollingTimer = setInterval(() => {
|
||||
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {
|
||||
return
|
||||
}
|
||||
if (!ordersQuery.isFetching.value) {
|
||||
void ordersQuery.refetch()
|
||||
}
|
||||
}, 10_000)
|
||||
}
|
||||
|
||||
watch(
|
||||
shouldPollOrders,
|
||||
(enabled) => {
|
||||
if (enabled) {
|
||||
startOrderPolling()
|
||||
} else {
|
||||
stopOrderPolling()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onBeforeUnmount(stopOrderPolling)
|
||||
|
||||
function mediaNames(order: MediaSupplyOrderDetail): string[] {
|
||||
const seen = new Set<string>()
|
||||
const names: string[] = []
|
||||
@@ -233,15 +249,6 @@ function formatOrderError(message?: string | null): string {
|
||||
<ReloadOutlined :class="{ 'refresh-spinning': ordersQuery.isFetching.value }" />
|
||||
刷新
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-btn-sync"
|
||||
:disabled="syncBacklinksMutation.isPending.value"
|
||||
@click="syncBacklinks"
|
||||
>
|
||||
<ReloadOutlined v-if="syncBacklinksMutation.isPending.value" class="refresh-spinning" />
|
||||
同步状态/回链
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -320,7 +327,7 @@ function formatOrderError(message?: string | null): string {
|
||||
<span v-else>{{ backlinkSummary(record) }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'external_order_code'">
|
||||
{{ record.external_order_code || record.external_order_id || '--' }}
|
||||
{{ record.external_order_code || '--' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'created_at'">
|
||||
{{ formatDateTime(record.created_at) }}
|
||||
|
||||
Reference in New Issue
Block a user