完成上架相关优化、oss迁移

This commit is contained in:
supernijia
2026-07-25 17:57:34 +08:00
parent 8ab36b1aaf
commit 4ddb8b47b0
28 changed files with 964 additions and 382 deletions
@@ -40,7 +40,6 @@
name="publish-country"
class="country-check-input"
:value="row.code"
:disabled="hasQueueWork"
/>
<span class="country-check-text">{{ row.label }} ({{ row.code }})</span>
</label>
@@ -54,13 +53,13 @@
v-for="row in COUNTRY_OPTIONS"
:key="row.code"
class="country-check-row"
:class="{ disabled: row.code === publishCountry || hasQueueWork }"
:class="{ disabled: row.code === publishCountry }"
>
<input
type="checkbox"
class="country-check-input"
:checked="syncCountries.includes(row.code)"
:disabled="row.code === publishCountry || hasQueueWork"
:disabled="row.code === publishCountry"
@change="onSyncCountryChange(row.code, $event)"
/>
<span class="country-check-text">{{ row.label }} ({{ row.code }})</span>
@@ -75,10 +74,10 @@
<button
type="button"
class="btn-run"
:disabled="parsing || uploading || hasQueueWork || !uploadedFiles.length"
:disabled="parsing || uploading || !uploadedFiles.length"
@click="submitRun"
>
{{ parsing ? '解析中...' : queueWorkerRunning ? '串行上架中...' : '开始上架' }}
{{ parsing ? '解析中...' : queueWorkerRunning || hasQueueWork ? '加入等待队列' : '开始上架' }}
</button>
<span class="loading-msg">{{ operationHint }}</span>
</div>
@@ -231,7 +230,7 @@ interface PublishDispatchOptions {
ziniaoVersion: ZiniaoVersion
}
interface StoredPublishQueue {
interface PublishQueueBatch {
taskId: number
pendingFileIds: number[]
activeFileId: number | null
@@ -239,6 +238,11 @@ interface StoredPublishQueue {
options: PublishDispatchOptions
}
interface StoredPublishQueueState {
active: PublishQueueBatch | null
pending: PublishQueueBatch[]
}
const selectedPaths = ref<string[]>([])
const uploadedFiles = ref<UploadedJavaFile[]>([])
const uploading = ref(false)
@@ -253,6 +257,7 @@ const currentFiles = ref<PublishFileItem[]>([])
const pendingFileIds = ref<number[]>([])
const activeFileId = ref<number | null>(null)
const dispatchOptions = ref<PublishDispatchOptions | null>(null)
const queuedBatches = ref<PublishQueueBatch[]>([])
const taskSnapshots = ref<Record<number, PublishTaskDetailVo>>({})
const historyItems = ref<PublishTaskDetailVo[]>([])
const missingTaskIds = ref<number[]>([])
@@ -355,8 +360,9 @@ const progressLoop = useTaskProgressLoop<PublishTaskDetailVo>({
})
const displayPaths = computed(() => selectedPaths.value.slice(0, 8))
const hasQueueWork = computed(() => pendingFileIds.value.length > 0 || activeFileId.value != null)
const selectionDisabled = computed(() => uploading.value || parsing.value || hasQueueWork.value)
const activeBatchHasWork = computed(() => pendingFileIds.value.length > 0 || activeFileId.value != null)
const hasQueueWork = computed(() => activeBatchHasWork.value || queuedBatches.value.length > 0)
const selectionDisabled = computed(() => uploading.value || parsing.value)
const currentDetail = computed<PublishTaskDetailVo | null>(() => {
const taskId = currentTaskId.value
@@ -512,13 +518,12 @@ async function submitRun() {
})
if (!parsed.taskId) throw new Error('后端未返回有效任务标识')
currentTaskId.value = parsed.taskId
currentFiles.value = parsed.files || []
dispatchOptions.value = {
const options: PublishDispatchOptions = {
publishCountry: publishCountry.value,
syncCountries: syncCountries.value.filter((country) => country !== publishCountry.value),
ziniaoVersion: ziniaoVersion.value,
}
const files = parsed.files || []
applyTaskSnapshot(parsed.taskId, {
task: {
id: parsed.taskId,
@@ -527,26 +532,44 @@ async function submitRun() {
sourceFileCount: parsed.sourceFileCount,
totalRows: parsed.totalRows,
},
files: parsed.files || [],
files,
result: parsed.result,
})
pendingFileIds.value = currentFiles.value
const batch: PublishQueueBatch = {
taskId: parsed.taskId,
pendingFileIds: files
.filter((file) => !isTerminalStatus(file.status) && isMatchedFile(file))
.map((file) => file.fileId)
.filter((fileId) => Number.isFinite(fileId) && fileId > 0)
activeFileId.value = null
saveQueueState()
.filter((fileId) => Number.isFinite(fileId) && fileId > 0),
activeFileId: null,
files,
options,
}
progressLoop.add(parsed.taskId)
selectedPaths.value = []
uploadedFiles.value = []
await Promise.all([loadDashboard(), loadHistory()])
if (!pendingFileIds.value.length) {
if (!batch.pendingFileIds.length) {
queueMessage.value = '解析完成,当前没有匹配成功且可执行的文件。'
ElMessage.warning(queueMessage.value)
return
}
queueMessage.value = `批次 ${parsed.taskNo || parsed.taskId} 已创建,开始串行处理 ${pendingFileIds.value.length} 个文件。`
const shouldWait = queueWorkerRunning.value || activeBatchHasWork.value || queuedBatches.value.length > 0
if (shouldWait) {
queuedBatches.value.push(batch)
saveQueueState()
queueMessage.value = `批次 ${parsed.taskNo || parsed.taskId} 已加入等待队列,前面还有 ${queuedBatches.value.length} 个批次`
ElMessage.success(queueMessage.value)
if (!queueWorkerRunning.value) void processQueue()
return
}
setActiveBatch(batch)
saveQueueState()
queueMessage.value = `批次 ${parsed.taskNo || parsed.taskId} 已创建,开始串行处理 ${batch.pendingFileIds.length} 个文件。`
ElMessage.success(queueMessage.value)
void processQueue()
} catch (error) {
@@ -560,37 +583,85 @@ async function submitRun() {
function saveQueueState() {
if (typeof window === 'undefined') return
if (!currentTaskId.value || !dispatchOptions.value || !hasQueueWork.value) {
const active: PublishQueueBatch | null = currentTaskId.value && dispatchOptions.value && activeBatchHasWork.value
? {
taskId: currentTaskId.value,
pendingFileIds: pendingFileIds.value,
activeFileId: activeFileId.value,
files: currentFiles.value,
options: dispatchOptions.value,
}
: null
const pending = queuedBatches.value.filter(
(batch) => batch.pendingFileIds.length > 0 || batch.activeFileId != null,
)
if (!active && !pending.length) {
window.localStorage.removeItem(queueStorageKey())
return
}
const state: StoredPublishQueue = {
taskId: currentTaskId.value,
pendingFileIds: pendingFileIds.value,
activeFileId: activeFileId.value,
files: currentFiles.value,
options: dispatchOptions.value,
}
const state: StoredPublishQueueState = { active, pending }
window.localStorage.setItem(queueStorageKey(), JSON.stringify(state))
}
function setActiveBatch(batch: PublishQueueBatch, restoreForm = false) {
currentTaskId.value = batch.taskId
currentFiles.value = batch.files
pendingFileIds.value = [...batch.pendingFileIds]
activeFileId.value = batch.activeFileId
dispatchOptions.value = batch.options
if (restoreForm) {
publishCountry.value = batch.options.publishCountry
syncCountries.value = [...batch.options.syncCountries]
ziniaoVersion.value = batch.options.ziniaoVersion
}
}
function takeNextBatch() {
while (queuedBatches.value.length) {
const next = queuedBatches.value.shift()
if (!next) break
if (
missingTaskIds.value.includes(next.taskId)
|| isTerminalStatus(taskSnapshots.value[next.taskId]?.task?.status)
) {
continue
}
setActiveBatch(next)
saveQueueState()
return true
}
saveQueueState()
return false
}
function loadQueueState() {
if (typeof window === 'undefined') return
try {
const raw = window.localStorage.getItem(queueStorageKey())
if (!raw) return
const state = JSON.parse(raw) as StoredPublishQueue
if (!Number.isFinite(state.taskId) || state.taskId <= 0 || !state.options) return
currentTaskId.value = state.taskId
pendingFileIds.value = (state.pendingFileIds || []).filter((id) => Number.isFinite(id) && id > 0)
activeFileId.value = Number.isFinite(state.activeFileId) && Number(state.activeFileId) > 0
? Number(state.activeFileId)
: null
currentFiles.value = Array.isArray(state.files) ? state.files : []
dispatchOptions.value = state.options
publishCountry.value = state.options.publishCountry
syncCountries.value = [...state.options.syncCountries]
ziniaoVersion.value = state.options.ziniaoVersion
const state = JSON.parse(raw) as Partial<StoredPublishQueueState> & Partial<PublishQueueBatch>
const active = state.active && Number.isFinite(state.active.taskId) && state.active.taskId > 0
? state.active
: !state.active && Number.isFinite(state.taskId) && Number(state.taskId) > 0 && state.options
? {
taskId: Number(state.taskId),
pendingFileIds: state.pendingFileIds || [],
activeFileId: state.activeFileId ?? null,
files: state.files || [],
options: state.options,
}
: null
if (active) setActiveBatch(active, true)
queuedBatches.value = (Array.isArray(state.pending) ? state.pending : [])
.filter((batch) => Number.isFinite(batch.taskId) && batch.taskId > 0 && batch.options)
.map((batch) => ({
...batch,
pendingFileIds: (batch.pendingFileIds || []).filter((id) => Number.isFinite(id) && id > 0),
activeFileId: Number.isFinite(batch.activeFileId) && Number(batch.activeFileId) > 0
? Number(batch.activeFileId)
: null,
files: Array.isArray(batch.files) ? batch.files : [],
}))
} catch {
window.localStorage.removeItem(queueStorageKey())
}
@@ -711,7 +782,7 @@ async function waitForFileTerminal(taskId: number, fileId: number) {
}
async function processQueue() {
if (disposed || queueWorkerRunning.value || !currentTaskId.value || !dispatchOptions.value) return
if (disposed || queueWorkerRunning.value || (!activeBatchHasWork.value && !queuedBatches.value.length)) return
const api = getPywebviewApi()
if (!api?.enqueue_json) {
queueMessage.value = '当前客户端未提供 enqueue_json,无法派发上架任务。'
@@ -719,76 +790,88 @@ async function processQueue() {
return
}
const taskId = currentTaskId.value
queueWorkerRunning.value = true
progressLoop.add(taskId)
try {
await activatePublishTask(taskId)
while (!disposed && (activeFileId.value != null || pendingFileIds.value.length > 0)) {
if (
missingTaskIds.value.includes(taskId)
|| isTerminalStatus(taskSnapshots.value[taskId]?.task?.status)
) {
pendingFileIds.value = []
activeFileId.value = null
saveQueueState()
break
}
if (activeFileId.value != null) {
const restoredFileId = activeFileId.value
queueMessage.value = `正在等待文件 ${getCurrentFile(restoredFileId)?.sourceFilename || restoredFileId} 完成...`
const restoredStatus = await waitForFileTerminal(taskId, restoredFileId)
if (restoredStatus === 'STOPPED') return
activeFileId.value = null
saveQueueState()
continue
while (!disposed) {
if (!currentTaskId.value || !dispatchOptions.value || !activeBatchHasWork.value) {
if (!takeNextBatch()) break
}
const nextFileId = pendingFileIds.value.shift()
if (!nextFileId) break
const file = getCurrentFile(nextFileId)
if (!file || isTerminalStatus(file.status)) {
saveQueueState()
continue
}
const taskId = currentTaskId.value
if (!taskId || !dispatchOptions.value) continue
progressLoop.add(taskId)
await activatePublishTask(taskId)
activeFileId.value = nextFileId
saveQueueState()
try {
await activatePublishFile(taskId, nextFileId)
updateCurrentFile(nextFileId, { status: 'RUNNING', progressMessage: '已派发到 Python 队列' })
const payload = buildQueuePayload(taskId, file)
const result = await api.enqueue_json(payload)
if (!result?.success) throw new Error(result?.error || 'Python 队列拒绝接收任务')
queueMessage.value = pendingFileIds.value.length
? `文件 ${file.sourceFilename || nextFileId} 已入队,完成后继续剩余 ${pendingFileIds.value.length} 个文件。`
: `文件 ${file.sourceFilename || nextFileId} 已入队,等待执行完成。`
const finalStatus = await waitForFileTerminal(taskId, nextFileId)
if (finalStatus === 'STOPPED') return
queueMessage.value = `文件 ${file.sourceFilename || nextFileId} ${['SUCCESS', 'COMPLETED'].includes(finalStatus) ? '已完成' : '执行失败'},继续下一个文件。`
activeFileId.value = null
saveQueueState()
} catch (error) {
const reason = error instanceof Error ? error.message : '文件派发失败'
try {
await submitDispatchFailure(taskId, file, reason)
} catch (compensationError) {
const message = compensationError instanceof Error ? compensationError.message : '失败状态提交失败'
queueMessage.value = `文件 ${file.sourceFilename || nextFileId} 派发失败,后端未确认失败状态:${message}。串行队列已暂停。`
while (!disposed && (activeFileId.value != null || pendingFileIds.value.length > 0)) {
if (
missingTaskIds.value.includes(taskId)
|| isTerminalStatus(taskSnapshots.value[taskId]?.task?.status)
) {
pendingFileIds.value = []
activeFileId.value = null
saveQueueState()
throw new Error(queueMessage.value)
break
}
if (activeFileId.value != null) {
const restoredFileId = activeFileId.value
queueMessage.value = `正在等待文件 ${getCurrentFile(restoredFileId)?.sourceFilename || restoredFileId} 完成...`
const restoredStatus = await waitForFileTerminal(taskId, restoredFileId)
if (restoredStatus === 'STOPPED') return
activeFileId.value = null
saveQueueState()
continue
}
queueMessage.value = `文件 ${file.sourceFilename || nextFileId} 派发失败,已记录并继续下一个文件。`
activeFileId.value = null
saveQueueState()
}
}
if (!disposed) {
queueMessage.value = '本批次文件已全部按顺序派发完成,正在生成结果文件。'
const nextFileId = pendingFileIds.value.shift()
if (!nextFileId) break
const file = getCurrentFile(nextFileId)
if (!file || isTerminalStatus(file.status)) {
saveQueueState()
continue
}
activeFileId.value = nextFileId
saveQueueState()
try {
await activatePublishFile(taskId, nextFileId)
updateCurrentFile(nextFileId, { status: 'RUNNING', progressMessage: '已派发到 Python 队列' })
const payload = buildQueuePayload(taskId, file)
const result = await api.enqueue_json(payload)
if (!result?.success) throw new Error(result?.error || 'Python 队列拒绝接收任务')
queueMessage.value = pendingFileIds.value.length
? `文件 ${file.sourceFilename || nextFileId} 已入队,完成后继续剩余 ${pendingFileIds.value.length} 个文件。`
: `文件 ${file.sourceFilename || nextFileId} 已入队,等待执行完成。`
const finalStatus = await waitForFileTerminal(taskId, nextFileId)
if (finalStatus === 'STOPPED') return
queueMessage.value = `文件 ${file.sourceFilename || nextFileId} ${['SUCCESS', 'COMPLETED'].includes(finalStatus) ? '已完成' : '执行失败'},继续下一个文件。`
activeFileId.value = null
saveQueueState()
} catch (error) {
const reason = error instanceof Error ? error.message : '文件派发失败'
try {
await submitDispatchFailure(taskId, file, reason)
} catch (compensationError) {
const message = compensationError instanceof Error ? compensationError.message : '失败状态提交失败'
queueMessage.value = `文件 ${file.sourceFilename || nextFileId} 派发失败,后端未确认失败状态:${message}。串行队列已暂停。`
saveQueueState()
throw new Error(queueMessage.value)
}
queueMessage.value = `文件 ${file.sourceFilename || nextFileId} 派发失败,已记录并继续下一个文件。`
activeFileId.value = null
saveQueueState()
}
}
if (disposed) return
await progressLoop.refreshOnce()
await Promise.all([loadDashboard(), loadHistory()])
ElMessage.success('上架批次已完成串行派发')
if (queuedBatches.value.length) {
queueMessage.value = `当前批次已完成派发,继续处理后续 ${queuedBatches.value.length} 个等待批次。`
continue
}
queueMessage.value = '所有上架批次已按顺序派发完成,正在生成结果文件。'
ElMessage.success('上架等待队列已完成派发')
break
}
} catch (error) {
if (disposed) return
@@ -825,34 +908,57 @@ async function loadHistory() {
}
function reconcileStoredQueue() {
function reconcileBatch(batch: PublishQueueBatch) {
if (missingTaskIds.value.includes(batch.taskId)) return null
const snapshot = taskSnapshots.value[batch.taskId]
if (isTerminalStatus(snapshot?.task?.status)) return null
const files = mergeFiles(batch.files || [], snapshot?.files || [])
let pendingIds = (batch.pendingFileIds || []).filter((fileId) => {
const file = files.find((item) => item.fileId === fileId)
return file && !isTerminalStatus(file.status)
})
let activeId = batch.activeFileId
if (activeId != null) {
const activeStatus = normalizeStatus(files.find((file) => file.fileId === activeId)?.status)
if (!activeStatus || activeStatus === 'PENDING') {
pendingIds = [activeId, ...pendingIds.filter((fileId) => fileId !== activeId)]
activeId = null
} else if (isTerminalStatus(activeStatus)) {
activeId = null
}
}
if (!pendingIds.length && activeId == null) return null
return { ...batch, pendingFileIds: pendingIds, activeFileId: activeId, files }
}
const taskId = currentTaskId.value
if (!taskId) return
if (missingTaskIds.value.includes(taskId)) {
const active = taskId && dispatchOptions.value
? reconcileBatch({
taskId,
pendingFileIds: pendingFileIds.value,
activeFileId: activeFileId.value,
files: currentFiles.value,
options: dispatchOptions.value,
})
: null
if (active) {
setActiveBatch(active)
} else {
currentTaskId.value = null
currentFiles.value = []
pendingFileIds.value = []
activeFileId.value = null
saveQueueState()
queueMessage.value = `任务 ${taskId} 已不存在,本地队列已清理。`
return
dispatchOptions.value = null
}
if (isTerminalStatus(taskSnapshots.value[taskId]?.task?.status)) {
pendingFileIds.value = []
activeFileId.value = null
saveQueueState()
return
}
pendingFileIds.value = pendingFileIds.value.filter((fileId) => {
const file = getCurrentFile(fileId)
return file && !isTerminalStatus(file.status)
})
if (activeFileId.value != null) {
const activeId = activeFileId.value
const activeStatus = normalizeStatus(getCurrentFile(activeId)?.status)
queuedBatches.value = queuedBatches.value
.map(reconcileBatch)
.filter((batch): batch is PublishQueueBatch => batch != null)
if (active?.activeFileId != null) {
const activeId = active.activeFileId
const activeStatus = normalizeStatus(active.files.find((file) => file.fileId === activeId)?.status)
if (!activeStatus || activeStatus === 'PENDING') {
pendingFileIds.value = [activeId, ...pendingFileIds.value.filter((fileId) => fileId !== activeId)]
activeFileId.value = null
queueMessage.value = `文件 ${activeId} 尚未激活,已恢复到待派发队列。`
} else if (isTerminalStatus(activeStatus)) {
activeFileId.value = null
}
}
saveQueueState()
@@ -935,13 +1041,14 @@ async function downloadResult(detail: PublishTaskDetailVo) {
onMounted(async () => {
loadQueueState()
if (currentTaskId.value) progressLoop.add(currentTaskId.value)
for (const batch of queuedBatches.value) progressLoop.add(batch.taskId)
await Promise.all([loadDashboard(), loadHistory()])
if (currentTaskId.value) await progressLoop.refreshOnce()
if (currentTaskId.value || queuedBatches.value.length) await progressLoop.refreshOnce()
reconcileStoredQueue()
if (hasQueueWork.value) {
queueMessage.value = activeFileId.value != null
? `检测到未完成文件 ${activeFileId.value},继续等待完成后衔接后续文件。`
: `检测到未完成队列,继续处理剩余 ${pendingFileIds.value.length}文件`
: `检测到未完成队列,继续处理当前文件及后续 ${queuedBatches.value.length}批次`
void processQueue()
}
})