feat: queue and reconcile SYB purchase order numbers (#305)
This commit is contained in:
@@ -6,6 +6,7 @@ export function previewPurchaseTasks(data, options = {}) { return request({ url:
|
||||
export function matchPurchaseSpecsBatch(data) { return request({ url: '/api/admin/v1/purchase-tasks/batch-spec-match', method: 'post', data, timeout: 610000 }) }
|
||||
export function createPurchaseTasksBatch(data) { return request({ url: '/api/admin/v1/purchase-tasks/batch', method: 'post', data, timeout: 60000 }) }
|
||||
export function retryPurchaseTasksBatch(data) { return request({ url: '/api/admin/v1/purchase-tasks/batch-retry', method: 'post', data, timeout: 60000 }) }
|
||||
export function writebackPurchaseOrderNumbers(data) { return request({ url: '/api/admin/v1/purchase-tasks/syb-order-writeback', method: 'post', data }) }
|
||||
export function createStockPurchaseTask(data, options = {}) { return request({ url: '/api/admin/v1/purchase-tasks/stock', method: 'post', data, ...options }) }
|
||||
export function createPurchaseTask(data) { return request({ url: '/api/admin/v1/purchase-tasks', method: 'post', data, timeout: 60000 }) }
|
||||
export function getPurchaseMatching(taskId) { return request({ url: `/api/admin/v1/purchase-tasks/${taskId}/matching`, method: 'get' }) }
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
<el-form-item label="SYB 商品"><el-input v-model="query.sybProductId" class="syb-input" clearable placeholder="商品 ID" @keyup.enter="search" /></el-form-item>
|
||||
<el-form-item label="PDD 订单号"><el-input v-model="query.pddOrderNo" class="order-input" clearable placeholder="订单号" @keyup.enter="search" /></el-form-item>
|
||||
<el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="RefreshLeft" @click="reset">重置</el-button></el-form-item>
|
||||
<el-form-item class="retry-action"><span class="selected-copy">已选择 <strong>{{ retrySelection.length }}</strong> 条</span><el-button type="warning" :disabled="retrySelection.length === 0" :loading="retryDialog.saving" @click="openRetryDialog">重试({{ retrySelection.length }})</el-button></el-form-item>
|
||||
</el-form>
|
||||
<div class="batch-actions"><span class="selected-copy">已选择 <strong>{{ retrySelection.length }}</strong> 条</span><el-button type="warning" :disabled="retryCandidates.length === 0" :loading="retryDialog.saving" @click="openRetryDialog">重试({{ retryCandidates.length }})</el-button><el-button type="primary" plain :disabled="orderWritebackCandidates.length === 0" :loading="orderWriteback.saving" @click="submitOrderWriteback(orderWritebackCandidates)">批量回填 SYB({{ orderWritebackCandidates.length }})</el-button></div>
|
||||
|
||||
<el-table ref="taskTable" v-loading="loading" :data="tasks" row-key="id" border stripe empty-text="暂无采购任务" :row-class-name="retryRowClass" @selection-change="handleRetrySelection">
|
||||
<el-table-column type="selection" width="48" :selectable="isRetrySelectable" />
|
||||
<el-table-column type="selection" width="48" :selectable="isSelectable" />
|
||||
<el-table-column label="任务" width="105"><template #default="{ row }"><div class="primary">CG-{{ row.id }}</div><el-tag size="small" effect="plain" :type="row.executionMode === 'live' ? 'success' : 'info'">{{ modeLabel(row.executionMode) }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="类型" width="110"><template #default="{ row }"><el-tag size="small" effect="plain" :type="taskTypeTag(row.taskType)">{{ taskTypeLabel(row.taskType) }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="蝦皮订单号" min-width="160"><template #default="{ row }"><span class="order-number">{{ row.shopeeOrderNoSnapshot || '—' }}</span></template></el-table-column>
|
||||
@@ -26,6 +26,7 @@
|
||||
<el-table-column label="状态" width="150" align="center"><template #default="{ row }"><el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag><el-tooltip v-if="row.status === 'failed' && !row.retryable" :content="row.retryDisabledReason || '当前任务不能重试'" placement="top"><span class="retry-disabled" tabindex="0">不可重试</span></el-tooltip></template></el-table-column>
|
||||
<el-table-column label="规格匹配" min-width="150"><template #default="{ row }"><el-tag :type="matchingType(row.matching?.status)" effect="plain">{{ matchingLabel(row.matching?.status) }}</el-tag><div v-if="row.matching && !row.matching.executable" class="muted">{{ row.matching.reason || '完成前不可执行' }}</div></template></el-table-column>
|
||||
<el-table-column label="订单 / 支付" min-width="170"><template #default="{ row }"><div>{{ row.pddOrderNo || '尚未取得订单号' }}</div><div class="muted">{{ paymentLabel(row.paymentReviewStatus) }}</div></template></el-table-column>
|
||||
<el-table-column label="SYB 单号回填" min-width="170"><template #default="{ row }"><span>{{ orderWritebackLabel(row.orderWriteback?.status) }}</span><div class="muted">{{ row.orderWriteback?.reason || '' }}</div></template></el-table-column>
|
||||
<el-table-column label="创建时间" width="170"><template #default="{ row }">{{ formatTime(row.createdAt) }}</template></el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right"><template #default="{ row }"><el-button type="primary" link @click="openDetail(row)">详情</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
@@ -59,9 +60,11 @@
|
||||
<div class="state-card"><span>订单</span><strong>{{ detail.task.pddOrderNo || '尚未取得订单号' }}</strong><small>{{ detail.task.orderSubmittedAt ? `下单时间 ${formatTime(detail.task.orderSubmittedAt)}` : '获取订单号失败即采购失败' }}</small><small>实付价格:{{ detail.task.pddOrderAmountCent == null ? '尚未获取' : priceText(detail.task.pddOrderAmountCent, 'CNY') }}</small></div>
|
||||
<div class="state-card"><span>支付复核</span><strong>{{ paymentLabel(detail.task.paymentReviewStatus) }}</strong><small>{{ detail.task.paymentReviewedAt ? formatTime(detail.task.paymentReviewedAt) : '仅人工记录,系统不支付' }}</small></div>
|
||||
<div class="state-card"><span>物流</span><strong>{{ logisticsLabel(detail.task.logisticsStatus) }}</strong><small>{{ detail.task.trackingNo || '尚未取得快递单号' }}</small></div>
|
||||
<div v-if="detail.task.taskType !== 'stock'" class="state-card"><span>SYB 回填</span><strong>{{ writebackLabel(detail.task.writebackStatus) }}</strong><small>{{ detail.task.writebackAt ? formatTime(detail.task.writebackAt) : '由独立回填流程处理' }}</small></div>
|
||||
<div v-if="detail.task.taskType !== 'stock'" class="state-card"><span>SYB 物流回填</span><strong>{{ writebackLabel(detail.task.writebackStatus) }}</strong><small>{{ detail.task.writebackAt ? formatTime(detail.task.writebackAt) : '由独立回填流程处理' }}</small></div>
|
||||
</div></section>
|
||||
|
||||
<section><h3>SYB 单号回填</h3><div class="state-card"><strong>{{ orderWritebackLabel(detail.task.orderWriteback?.status) }}</strong><small>{{ detail.task.orderWriteback?.reason || '仅回填 PDD 订单号,不回填实付价格,不要求已支付。' }}</small><small>完成时间:{{ formatTime(detail.task.orderWriteback?.completedAt) }}</small><div><el-button v-if="canOrderWriteback(detail.task)" type="primary" plain :loading="orderWriteback.saving" @click="submitOrderWriteback([detail.task])">{{ detail.task.orderWriteback.status === 'not_started' ? '回填 SYB 单号' : '重试回填 SYB 单号' }}</el-button><el-button :loading="detail.loading" @click="refreshOrderWritebackDetail">刷新状态</el-button></div></div></section>
|
||||
|
||||
<section><h3>执行记录</h3><el-table :data="detail.attempts" border size="small" empty-text="暂无执行记录">
|
||||
<el-table-column label="次数" width="70"><template #default="{ row }">#{{ row.attemptNumber }}</template></el-table-column><el-table-column label="阶段" prop="phase" min-width="120" /><el-table-column label="状态" prop="status" min-width="110" /><el-table-column label="结果" min-width="150"><template #default="{ row }">{{ row.resultType || row.errorCode || '—' }}</template></el-table-column><el-table-column label="说明" min-width="220"><template #default="{ row }">{{ row.errorMessage || '—' }}</template></el-table-column><el-table-column label="开始时间" width="170"><template #default="{ row }">{{ formatTime(row.startedAt || row.createdAt) }}</template></el-table-column>
|
||||
</el-table></section>
|
||||
@@ -112,6 +115,12 @@
|
||||
<template #footer><el-button @click="retryResult.open=false">留在当前页</el-button><el-button v-if="firstRetriedTask" type="primary" @click="openRetriedTask(firstRetriedTask)">查看成功任务</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="orderWriteback.open" title="SYB 单号回填提交结果" width="760px" :close-on-click-modal="false">
|
||||
<el-alert title="待回填表示已受理,不代表 SYB 已写入。请刷新列表或详情查看最终状态;冲突项不会覆盖原单号。" type="info" :closable="false" show-icon class="notice" />
|
||||
<el-table :data="orderWriteback.items" border size="small" max-height="360" empty-text="暂无处理结果"><el-table-column label="任务" width="110"><template #default="{ row }">CG-{{ row.taskId }}</template></el-table-column><el-table-column label="结果" width="130"><template #default="{ row }">{{ orderWritebackLabel(row.result) }}</template></el-table-column><el-table-column label="说明" min-width="280" prop="reason" /></el-table>
|
||||
<template #footer><el-button @click="orderWriteback.open=false">关闭</el-button><el-button type="primary" :loading="loading" @click="load">刷新列表状态</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="cancelDialog.open" title="标记采购任务已取消" width="520px" :close-on-click-modal="false">
|
||||
<el-alert title="这里只记录人工已在 PDD 取消的结果,不会替你操作 PDD。" type="info" :closable="false" show-icon class="notice" />
|
||||
<el-form label-position="top"><el-form-item label="取消原因(必填)"><el-input v-model="cancelDialog.reason" type="textarea" :rows="3" maxlength="300" show-word-limit /></el-form-item></el-form>
|
||||
@@ -133,7 +142,7 @@
|
||||
<script>
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { authorizeRepurchase, cancelPurchaseTask, choosePurchaseMatching, getPurchaseTask, listPurchaseTasks, requeuePurchaseMatching, resolveUnknownPurchaseTask, retryPurchaseTasksBatch, reviewPurchasePayment, selectPurchaseWriteback } from '@/api/goauto/purchase-tasks'
|
||||
import { authorizeRepurchase, cancelPurchaseTask, choosePurchaseMatching, getPurchaseTask, listPurchaseTasks, requeuePurchaseMatching, resolveUnknownPurchaseTask, retryPurchaseTasksBatch, reviewPurchasePayment, selectPurchaseWriteback, writebackPurchaseOrderNumbers } from '@/api/goauto/purchase-tasks'
|
||||
import { createRequestId } from '@/utils/request-id'
|
||||
|
||||
const statusOptions = [
|
||||
@@ -147,6 +156,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false, loadError: '', tasks: [], total: 0, statuses: statusOptions,
|
||||
orderWriteback: { open: false, saving: false, items: [] },
|
||||
query: { page: 1, pageSize: 20, taskId: '', taskType: '', shopeeOrderNo: '', status: '', executionMode: 'live', sybProductId: '', pddOrderNo: '' },
|
||||
detail: { open: false, loading: false, task: null, attempts: [], matching: null },
|
||||
matchingAction: { saving: false }, matchingDialog: { open: false, saving: false, color: '', size: '' },
|
||||
@@ -159,6 +169,8 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
retryCandidates() { return this.retrySelection.filter(this.isRetrySelectable) },
|
||||
orderWritebackCandidates() { return this.retrySelection.filter(this.canOrderWriteback) },
|
||||
detailAlert() {
|
||||
const task = this.detail.task || {}
|
||||
if (task.status === 'order_result_unknown') return { type: 'error', title: '订单结果未知,禁止重复采购', description: '请先到 PDD 人工核对订单,再在本页处理结果。' }
|
||||
@@ -214,9 +226,26 @@ export default {
|
||||
formatTime(value) { if (!value) return '—'; return new Date(value).toLocaleString('zh-CN', { hour12: false }) },
|
||||
requestId() { return createRequestId() },
|
||||
isRetrySelectable(row) { return row.taskType !== 'stock' && row.status === 'failed' && row.retryable === true },
|
||||
canOrderWriteback(row) { return row.orderWriteback?.canSubmit === true },
|
||||
isSelectable(row) { return this.isRetrySelectable(row) || this.canOrderWriteback(row) },
|
||||
orderWritebackLabel(value) { return { not_started: '未发起', pending: '待回填', running: '回填中', succeeded: '已回填', failed: '回填失败', unknown: '结果待核对', conflict: '单号冲突', skipped: '已跳过', not_applicable: '不适用' }[value] || '未记录' },
|
||||
async refreshOrderWritebackDetail() { this.detail.loading = true; try { await this.refreshDetail() } finally { this.detail.loading = false } },
|
||||
async submitOrderWriteback(rows) {
|
||||
if (this.orderWriteback.saving) return
|
||||
const ids = rows.filter(this.canOrderWriteback).map(row => row.id)
|
||||
if (!ids.length) return
|
||||
this.orderWriteback.saving = true
|
||||
try {
|
||||
const r = await writebackPurchaseOrderNumbers({ requestId: this.requestId(), purchaseTaskIds: ids })
|
||||
this.orderWriteback.items = r.data.items || []
|
||||
this.orderWriteback.open = true
|
||||
await this.load()
|
||||
if (this.detail.open && this.detail.task) await this.refreshDetail()
|
||||
} catch (error) { ElMessage.error('回填提交结果未确认,请刷新任务状态后再试。') } finally { this.orderWriteback.saving = false }
|
||||
},
|
||||
retryRowClass({ row }) { return row.status === 'failed' && !row.retryable ? 'retry-disabled-row' : '' },
|
||||
handleRetrySelection(rows) { this.retrySelection = rows },
|
||||
openRetryDialog() { if (!this.retrySelection.length) return; this.retryDialog = { open: true, saving: false, items: [...this.retrySelection] } },
|
||||
openRetryDialog() { if (!this.retryCandidates.length) return; this.retryDialog = { open: true, saving: false, items: [...this.retryCandidates] } },
|
||||
async submitRetry() {
|
||||
if (!this.retryDialog.items.length) return
|
||||
this.retryDialog.saving = true
|
||||
@@ -251,5 +280,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.batch-actions{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:16px}.batch-actions .el-button+.el-button{margin-left:0}
|
||||
.page-card{min-height:calc(100vh - 124px)}.search-form{display:flex;align-items:flex-end;padding:16px 16px 0;margin-bottom:16px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc;white-space:nowrap}.retry-action{margin-left:auto;margin-right:0}.selected-copy{margin-right:10px;color:#606266}.selected-copy strong{color:#337ecc;font-variant-numeric:tabular-nums}.task-input{width:72px}.shopee-order-input{width:140px}.syb-input{width:100px}.order-input{width:120px}.order-number{font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.notice{margin-bottom:18px}.primary{color:#303133;font-weight:600}.muted{color:#909399;font-size:12px;line-height:1.5}.ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.image-search-tag{margin-left:6px;vertical-align:middle}.retry-disabled{display:block;margin-top:4px;color:#b88230;font-size:11px;line-height:1.2;cursor:help}.dialog-help{margin:14px 0 0;color:#606266;line-height:1.6}.drawer-body{min-height:240px;padding:0 4px 28px}.detail-heading{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:18px}.detail-heading>div{display:flex;align-items:center;gap:8px}.task-number{font-size:22px;font-weight:700;color:#1f2937}.drawer-body section{margin-top:24px}.drawer-body h3{margin:0 0 12px;color:#303133;font-size:17px}.state-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.state-card{display:flex;min-height:104px;flex-direction:column;gap:7px;padding:16px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc}.state-card span,.state-card small{color:#606266}.state-card strong{color:#1f2937;font-size:16px;overflow-wrap:anywhere}.drawer-actions{position:sticky;bottom:0;z-index:2;display:flex;justify-content:flex-end;gap:8px;margin:24px -4px -28px;padding:16px 4px;background:#fff;border-top:1px solid #e5e7eb}:deep(.el-table .retry-disabled-row){color:#909399;background:#fafafa}:deep(.el-table .retry-disabled-row .el-checkbox){cursor:not-allowed}@media(max-width:1200px){.search-form{overflow-x:auto}.retry-action{margin-left:24px}}@media(max-width:768px){.search-form{white-space:normal}.state-grid{grid-template-columns:1fr}.detail-heading{align-items:flex-start;flex-direction:column}.drawer-actions{flex-wrap:wrap}}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user