Files
ruoyi-vue/src/views/jarvis/wecomShareLinkLogistics/index.vue
2026-04-03 00:58:24 +08:00

344 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="88px">
<el-form-item label="发送人" prop="fromUserName">
<el-input v-model="queryParams.fromUserName" placeholder="企微 UserID" clearable style="width: 140px" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 120px">
<el-option label="待扫描" value="PENDING" />
<el-option label="等待运单/重试" value="WAITING" />
<el-option label="已推送" value="PUSHED" />
<el-option label="已放弃" value="ABANDONED" />
<el-option label="历史补录" value="IMPORTED" />
</el-select>
</el-form-item>
<el-form-item label="短链" prop="trackingUrl">
<el-input v-model="queryParams.trackingUrl" placeholder="3.cn 片段" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="运单号" prop="waybillNo">
<el-input v-model="queryParams.waybillNo" placeholder="模糊" clearable style="width: 120px" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="备注" prop="userRemark">
<el-input v-model="queryParams.userRemark" placeholder="模糊" clearable style="width: 160px" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="入队时间">
<el-date-picker
v-model="dateRange"
type="daterange"
value-format="yyyy-MM-dd"
range-separator=""
start-placeholder="开始"
end-placeholder="结束"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-alert
title="用于排查企微 3.cn 分享链物流WAITING + no_waybill_yet 未出单push_failed 推送未成功ABANDONED 超限IMPORTED 为从「企微消息跟踪」表补录,仅作留痕,不表示实时推送结果。"
type="info"
:closable="false"
show-icon
class="mb8"
/>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-video-play"
size="mini"
:loading="drainQueueLoading"
v-hasPermi="['jarvis:wecom:shareLinkLog:list']"
@click="handleDrainQueueOnce"
>执行待队列一轮</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-upload2"
size="mini"
:loading="backfillLoading"
v-hasPermi="['jarvis:wecom:shareLinkLog:import']"
@click="handleBackfill"
>从追踪补录历史</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="list" border>
<el-table-column label="ID" prop="id" width="72" />
<el-table-column label="发送人" prop="fromUserName" width="120" show-overflow-tooltip />
<el-table-column label="推送目标" prop="touserPush" width="140" show-overflow-tooltip />
<el-table-column label="状态" prop="status" width="100" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 'PUSHED'" type="success" size="small">{{ scope.row.status }}</el-tag>
<el-tag v-else-if="scope.row.status === 'WAITING'" type="warning" size="small">{{ scope.row.status }}</el-tag>
<el-tag v-else-if="scope.row.status === 'ABANDONED'" type="danger" size="small">{{ scope.row.status }}</el-tag>
<el-tag v-else-if="scope.row.status === 'IMPORTED'" type="info" size="small">{{ scope.row.status }}</el-tag>
<el-tag v-else type="info" size="small">{{ scope.row.status || '—' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="扫描次数" prop="scanAttempts" width="88" align="center" />
<el-table-column label="运单号" prop="waybillNo" width="140" show-overflow-tooltip />
<el-table-column label="用户备注" prop="userRemark" min-width="160" show-overflow-tooltip />
<el-table-column label="最近说明" prop="lastNote" min-width="140" show-overflow-tooltip />
<el-table-column label="短链" prop="trackingUrl" min-width="160" show-overflow-tooltip />
<el-table-column label="创建时间" prop="createTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="168" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-view" @click="openDetail(scope.row)">详情</el-button>
<el-button type="text" size="mini" icon="el-icon-truck" @click="handleFetchShareLink(scope.row)">获取物流</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<el-dialog title="获取物流信息(分享链)" :visible.sync="fetchLogisticsDialogVisible" width="720px" append-to-body @close="fetchLogisticsResult = null">
<div v-loading="fetchLogisticsLoading">
<el-alert
v-if="fetchLogisticsResult"
:title="fetchLogisticsResult.success ? '请求已完成' : '请求失败'"
:type="fetchLogisticsResult.success ? 'success' : 'error'"
:closable="false"
show-icon
class="mb8"
/>
<el-form v-if="fetchLogisticsResult && fetchLogisticsResult.success" label-width="120px" size="small">
<el-form-item label="jobKey"><span>{{ fetchLogisticsResult.jobKey }}</span></el-form-item>
<el-form-item label="终端成功"><span>{{ fetchLogisticsResult.terminalSuccess }}</span>已推送或已去重</el-form-item>
<el-form-item label="已发推送"><span>{{ fetchLogisticsResult.pushSent }}</span></el-form-item>
<el-form-item label="运单号" v-if="fetchLogisticsResult.waybillNo"><span>{{ fetchLogisticsResult.waybillNo }}</span></el-form-item>
<el-form-item label="说明" v-if="fetchLogisticsResult.adhocNote"><span>{{ fetchLogisticsResult.adhocNote }}</span></el-form-item>
<el-form-item label="物流短链"><span style="word-break:break-all">{{ fetchLogisticsResult.logisticsLink }}</span></el-form-item>
<el-form-item label="请求URL" v-if="fetchLogisticsResult.requestUrl">
<el-input type="textarea" :rows="2" readonly :value="fetchLogisticsResult.requestUrl" />
</el-form-item>
<el-form-item label="健康检查" v-if="fetchLogisticsResult.healthOk != null">
<span>{{ fetchLogisticsResult.healthOk }}</span>
<span v-if="fetchLogisticsResult.healthMessage"> {{ fetchLogisticsResult.healthMessage }}</span>
</el-form-item>
<el-form-item label="推送错误" v-if="fetchLogisticsResult.pushError">
<el-input type="textarea" :rows="3" readonly :value="fetchLogisticsResult.pushError" />
</el-form-item>
<el-form-item label="返回(原始)" v-if="fetchLogisticsResult.responseRaw">
<el-input type="textarea" :rows="5" readonly :value="fetchLogisticsResult.responseRaw" />
</el-form-item>
<el-form-item label="返回(解析)" v-if="fetchLogisticsResult.responseData">
<el-input type="textarea" :rows="8" readonly :value="formatJson(fetchLogisticsResult.responseData)" />
</el-form-item>
</el-form>
<el-form v-else-if="fetchLogisticsResult && !fetchLogisticsResult.success" label-width="100px" size="small">
<el-form-item label="错误"><span>{{ fetchLogisticsResult.error }}</span></el-form-item>
</el-form>
<span v-else-if="fetchLogisticsLoading" style="color:#999">正在请求</span>
</div>
<div slot="footer">
<el-button @click="fetchLogisticsDialogVisible = false">关闭</el-button>
<el-button type="primary" v-if="fetchLogisticsResult && fetchLogisticsResult.success" @click="copyFetchLogisticsResult">复制 JSON</el-button>
</div>
</el-dialog>
<el-dialog title="任务详情" :visible.sync="detailOpen" width="720px" append-to-body>
<el-descriptions v-if="detail" :column="1" border size="small">
<el-descriptions-item label="jobKey">{{ detail.jobKey }}</el-descriptions-item>
<el-descriptions-item label="发送人">{{ detail.fromUserName || '—' }}</el-descriptions-item>
<el-descriptions-item label="推送接收人">{{ detail.touserPush || '—' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ detail.status }}</el-descriptions-item>
<el-descriptions-item label="扫描次数">{{ detail.scanAttempts }}</el-descriptions-item>
<el-descriptions-item label="运单号">{{ detail.waybillNo || '—' }}</el-descriptions-item>
<el-descriptions-item label="最近说明">{{ detail.lastNote || '—' }}</el-descriptions-item>
<el-descriptions-item label="短链">
<span style="word-break:break-all">{{ detail.trackingUrl }}</span>
</el-descriptions-item>
<el-descriptions-item label="用户备注">
<pre class="trace-pre">{{ detail.userRemark || '(空)' }}</pre>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ parseTime(detail.createTime) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ parseTime(detail.updateTime) }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script>
import { listWecomShareLinkLogisticsJob, getWecomShareLinkLogisticsJob, backfillShareLinkLogisticsFromTrace, fetchShareLinkManually, drainShareLinkPendingQueueOnce } from '@/api/jarvis/wecomShareLinkLogistics'
export default {
name: 'WecomShareLinkLogistics',
data() {
return {
backfillLoading: false,
drainQueueLoading: false,
fetchLogisticsDialogVisible: false,
fetchLogisticsLoading: false,
fetchLogisticsResult: null,
loading: true,
total: 0,
list: [],
dateRange: [],
detailOpen: false,
detail: null,
queryParams: {
pageNum: 1,
pageSize: 10,
fromUserName: null,
status: null,
trackingUrl: null,
waybillNo: null,
userRemark: null
}
}
},
created() {
this.getList()
},
methods: {
getList() {
this.loading = true
const q = { ...this.queryParams, params: {} }
if (this.dateRange && this.dateRange.length === 2) {
q.params = { beginTime: this.dateRange[0], endTime: this.dateRange[1] }
}
listWecomShareLinkLogisticsJob(q).then(res => {
this.list = res.rows
this.total = res.total
this.loading = false
}).catch(() => { this.loading = false })
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
openDetail(row) {
getWecomShareLinkLogisticsJob(row.jobKey).then(res => {
this.detail = res.data
this.detailOpen = true
})
},
formatJson(v) {
if (v == null) return ''
if (typeof v === 'string') return v
try {
return JSON.stringify(v, null, 2)
} catch (e) {
return String(v)
}
},
async handleFetchShareLink(row) {
if (!row.trackingUrl || !String(row.trackingUrl).trim()) {
this.$message.warning('该任务暂无物流短链')
return
}
this.fetchLogisticsDialogVisible = true
this.fetchLogisticsLoading = true
this.fetchLogisticsResult = null
try {
const res = await fetchShareLinkManually({ jobKey: row.jobKey })
if (res.code === 200) {
this.fetchLogisticsResult = { success: true, ...res.data }
this.$message.success('已请求物流接口,列表状态已更新')
this.getList()
} else {
this.fetchLogisticsResult = { success: false, error: res.msg || '失败' }
this.$message.error(res.msg || '获取失败')
}
} catch (e) {
this.fetchLogisticsResult = { success: false, error: e.message || '请求异常' }
this.$message.error('获取物流失败: ' + (e.message || '未知错误'))
} finally {
this.fetchLogisticsLoading = false
}
},
copyFetchLogisticsResult() {
if (!this.fetchLogisticsResult) return
const t = JSON.stringify(this.fetchLogisticsResult, null, 2)
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(t).then(() => this.$message.success('已复制到剪贴板')).catch(() => this.fallbackCopy(t))
} else {
this.fallbackCopy(t)
}
},
fallbackCopy(text) {
const textArea = document.createElement('textarea')
textArea.value = text
textArea.style.position = 'fixed'
textArea.style.left = '-999999px'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
try {
document.execCommand('copy')
this.$message.success('已复制到剪贴板')
} catch (e) {
this.$message.error('复制失败')
}
document.body.removeChild(textArea)
},
handleDrainQueueOnce() {
this.$modal.confirm('立即执行一轮 Redis 待扫描队列(与定时任务末尾逻辑相同,每批条数受后端 adhoc-pending-batch-size 限制)?').then(() => {
this.drainQueueLoading = true
return drainShareLinkPendingQueueOnce()
}).then(res => {
const d = res.data || {}
this.$modal.msgSuccess('已弹出处理 ' + (d.processedFromQueue != null ? d.processedFromQueue : 0) + ' 条')
this.getList()
}).catch(() => {}).finally(() => { this.drainQueueLoading = false })
},
handleBackfill() {
this.$modal.confirm('从「企微消息跟踪」补录历史分享链任务(状态 IMPORTED已存在的 jobKey 将跳过。是否继续?').then(() => {
this.backfillLoading = true
return backfillShareLinkLogisticsFromTrace()
}).then(res => {
const d = res.data || {}
const parts = [
'扫描 ' + (d.scannedRemarkDoneRows != null ? d.scannedRemarkDoneRows : '—') + ' 条',
'新增 ' + (d.imported != null ? d.imported : 0),
'跳过已有 ' + (d.skippedDuplicate != null ? d.skippedDuplicate : 0),
'无短链 ' + (d.skippedNoUrl != null ? d.skippedNoUrl : 0)
]
this.$modal.msgSuccess(parts.join(''))
this.getList()
}).catch(() => {}).finally(() => { this.backfillLoading = false })
}
}
}
</script>
<style scoped>
.trace-pre {
white-space: pre-wrap;
word-break: break-word;
margin: 0;
font-family: inherit;
font-size: 13px;
max-height: 280px;
overflow: auto;
}
.mb8 {
margin-bottom: 8px;
}
</style>