后台店铺数据记录页新增 Tab 切换与重复 ASIN 分析;task-169/170 HTTP 客户端配置接入;dedupe/brand 相关改造
Build Backend JAR / build (push) Has been cancelled
Build Backend JAR / build (push) Has been cancelled
- 管理后台:店铺数据记录改为列表分页展示(去任务号/文件列/累计标题,'最新'改'更新时间'),新增'重复 ASIN' Tab 按跨店聚合展示 ASIN/店铺/分组/国家/日期/价格;修复失败/进行中任务被过滤不显示的问题 - task-169: BrandCheckHttpConfigResolver 从 aiimage.http-client.* 读取品牌检查超时/重试(默认同现状、钳制),附 8 条测试 - task-170: surefire 内存调整为 1536m - dedupe: DedupeRunService 重构(事务边界/进度)、新增 DedupeRunProgressVo、Controller 与结果 VO 调整,PermissionMenuService 相应适配 - brand/dedupe 前端:BrandDedupeTab/BrandPatrolDeleteTab 改造、新增 CountrySelector 组件与 country-options、类型与接口端对齐、测试更新 - 移除无引用文件:backend/static/logo.jpg、prompts/
This commit is contained in:
@@ -79,7 +79,7 @@
|
||||
{{ cleanRunning ? '清洗中...' : '开始清洗' }}
|
||||
</button>
|
||||
<span class="loading-msg">
|
||||
{{ cleanRunning ? '正在处理文件并上传结果,请稍候…' : '选择文件、列和 ID 规则后即可执行清洗,结果会显示在右侧供下载' }}
|
||||
{{ cleanRunning ? `正在处理文件并上传结果,已处理 ${cleanProgressProcessed}/${cleanSummary.total || 0} 个文件…` : '选择文件、列和 ID 规则后即可执行清洗,结果会显示在右侧供下载' }}
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -148,7 +148,7 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import BrandTopBar from './BrandTopBar.vue'
|
||||
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
|
||||
import { deleteDedupeHistory, getDedupeHistory, getDedupeResultDownloadUrl, getExcelInfo, runDedupe, type DedupeResultItem, type DedupeRunVo } from '@/shared/api/java-modules'
|
||||
import { deleteDedupeHistory, getDedupeHistory, getDedupeResultDownloadUrl, getDedupeRunProgress, getExcelInfo, runDedupe, type DedupeResultItem, type DedupeRunProgressVo, type DedupeRunVo } from '@/shared/api/java-modules'
|
||||
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
||||
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
|
||||
import { EXCEL_EXTENSIONS, checkSelectedFiles, guardBlocked } from '@/shared/dispatch-guard'
|
||||
@@ -163,6 +163,7 @@ const cleanKeepIntegerIds = ref(false)
|
||||
const cleanKeepUnderscoreIds = ref(true)
|
||||
const cleanKeepIntegerMainIdsWhenNoSubIds = ref(true)
|
||||
const cleanRunning = ref(false)
|
||||
const cleanProgressProcessed = ref(0)
|
||||
const cleanResultItems = ref<DedupeResultItem[]>([])
|
||||
const cleanSummary = ref<DedupeRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
|
||||
const cleanDisplayPaths = computed(() => cleanSelectedPaths.value.slice(0, 8))
|
||||
@@ -290,7 +291,7 @@ async function submitCleanRun() {
|
||||
|
||||
try {
|
||||
cleanRunning.value = true
|
||||
const result = await runDedupe({
|
||||
const progress = await runDedupe({
|
||||
files: cleanUploadedFiles.value.map((item) => ({ fileKey: item.fileKey, originalFilename: item.originalFilename, relativePath: item.relativePath })),
|
||||
selectedColumns: cleanSelectedColumns.value,
|
||||
keepIntegerIds: cleanKeepIntegerIds.value,
|
||||
@@ -301,6 +302,15 @@ async function submitCleanRun() {
|
||||
? cleanArchiveName.value
|
||||
: undefined,
|
||||
})
|
||||
const result = await pollDedupeRunProgress(progress.runId, (latest) => {
|
||||
cleanProgressProcessed.value = latest.processedCount
|
||||
cleanSummary.value = {
|
||||
total: latest.total,
|
||||
successCount: latest.successCount,
|
||||
failedCount: latest.failedCount,
|
||||
items: cleanResultItems.value,
|
||||
}
|
||||
})
|
||||
cleanSummary.value = result
|
||||
cleanResultItems.value = result.items || []
|
||||
await loadCleanHistory()
|
||||
@@ -325,6 +335,36 @@ async function submitCleanRun() {
|
||||
}
|
||||
}
|
||||
|
||||
// 去重任务进度轮询:2 秒一次,10 分钟超时(超时任务由后端继续执行,结果可在历史列表中查看)
|
||||
const DEDUPE_POLL_INTERVAL_MS = 2000
|
||||
const DEDUPE_POLL_TIMEOUT_MS = 10 * 60 * 1000
|
||||
|
||||
async function pollDedupeRunProgress(runId: string, onProgress?: (progress: DedupeRunProgressVo) => void): Promise<DedupeRunVo> {
|
||||
const deadline = Date.now() + DEDUPE_POLL_TIMEOUT_MS
|
||||
while (true) {
|
||||
const progress = await getDedupeRunProgress(runId)
|
||||
if (progress.status === 'not_found') {
|
||||
throw new Error('去重任务不存在或已过期')
|
||||
}
|
||||
if (progress.status !== 'running') {
|
||||
if (progress.status === 'failed') {
|
||||
throw new Error(progress.error || '去重任务执行失败')
|
||||
}
|
||||
if (!progress.result) {
|
||||
throw new Error(progress.error || '去重任务未返回结果')
|
||||
}
|
||||
return progress.result
|
||||
}
|
||||
onProgress?.(progress)
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(
|
||||
`去重任务仍在处理中(已处理 ${progress.processedCount}/${progress.total} 个文件),请稍后在历史列表中查看结果`,
|
||||
)
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, DEDUPE_POLL_INTERVAL_MS))
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCleanHistory() {
|
||||
try {
|
||||
const response = await getDedupeHistory()
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="section-title">店铺输入</div>
|
||||
<div class="input-zone">
|
||||
<div class="hint">
|
||||
左侧负责录入店铺并加入备选区,点击“匹配店铺”只会生成匹配结果;确认后需手动点击“推送到 Python 队列”才会创建并执行任务。
|
||||
左侧负责录入店铺并加入备选区,点击“匹配店铺”只会生成匹配结果;确认后需手动点击“推送到 Python 队列”才会创建并执行任务。任务只会跑下方勾选的国家。
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<el-input
|
||||
@@ -113,6 +113,12 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<CountrySelector
|
||||
v-model="selectedCountryCodes"
|
||||
title="巡店国家与顺序"
|
||||
min-selected-warning="至少保留 1 个国家,否则任务没有可巡查的站点"
|
||||
/>
|
||||
|
||||
<ZiniaoVersionSetting v-model="ziniaoVersion" />
|
||||
|
||||
<div class="run-row">
|
||||
@@ -363,7 +369,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import BrandTopBar from "@/pages/brand/components/BrandTopBar.vue";
|
||||
import {
|
||||
@@ -399,8 +405,13 @@ import { checkQueuePayload } from "@/shared/dispatch-guard";
|
||||
import { passGuard } from "@/shared/dispatch-guard-ui";
|
||||
import ZiniaoVersionSetting from "@/shared/components/ZiniaoVersionSetting.vue";
|
||||
import { useZiniaoVersion } from "@/shared/utils/ziniao-version";
|
||||
import CountrySelector from "@/shared/components/CountrySelector.vue";
|
||||
import {
|
||||
EU_COUNTRY_CODES,
|
||||
countryLabel,
|
||||
sanitizeCountryCodes,
|
||||
} from "@/shared/country-options";
|
||||
|
||||
const COUNTRY_TEMPLATE = ["德国", "英国", "法国", "意大利", "西班牙"] as const;
|
||||
const MAX_TRANSIENT_ERRORS = 30;
|
||||
const ziniaoVersion = useZiniaoVersion();
|
||||
|
||||
@@ -410,6 +421,7 @@ const candidates = ref<PatrolDeleteCandidateVo[]>([]);
|
||||
const selectedCandidates = ref<PatrolDeleteCandidateVo[]>([]);
|
||||
const deleteConditions = ref<PatrolDeleteConditionVo[]>([]);
|
||||
const selectedConditionIds = ref<number[]>([]);
|
||||
const selectedCountryCodes = ref<string[]>([...EU_COUNTRY_CODES]);
|
||||
const matchedItems = ref<PatrolDeleteShopQueueItem[]>([]);
|
||||
const historyItems = ref<PatrolDeleteHistoryItem[]>([]);
|
||||
const dashboard = ref<PatrolDeleteDashboardVo>({
|
||||
@@ -432,6 +444,10 @@ const timers = createCategorizedTimers("patrol-delete");
|
||||
const matchedRunnableItems = computed(() =>
|
||||
matchedItems.value.filter((item) => item.matched),
|
||||
);
|
||||
// 模板结构以中文国家名为 key(Java 的 Excel 列、Python 的站点切换都按中文名匹配)
|
||||
const selectedCountryNames = computed(() =>
|
||||
selectedCountryCodes.value.map((code) => countryLabel(code)),
|
||||
);
|
||||
const taskRecordItems = computed(() => groupHistoryItemsByTask(historyItems.value));
|
||||
const currentSectionItems = computed(() =>
|
||||
taskRecordItems.value.filter((item) => !isTaskTerminal(item.taskStatus)),
|
||||
@@ -459,6 +475,10 @@ function queueStateStorageKey() {
|
||||
return `patrol-delete:queue-state:${uidForStorage()}`;
|
||||
}
|
||||
|
||||
function countryCodesStorageKey() {
|
||||
return `patrol-delete:country-codes:${uidForStorage()}`;
|
||||
}
|
||||
|
||||
function rowKeyForMatch(row: PatrolDeleteShopQueueItem) {
|
||||
return `${(row.shopName || "").trim()}::${row.shopId || ""}`;
|
||||
}
|
||||
@@ -559,7 +579,7 @@ function formatMatchRemark(row: PatrolDeleteShopQueueItem) {
|
||||
}
|
||||
|
||||
function buildTemplateCountrySections(): PatrolDeleteCountrySection[] {
|
||||
return COUNTRY_TEMPLATE.map((country) => ({
|
||||
return selectedCountryNames.value.map((country) => ({
|
||||
country,
|
||||
rows: [
|
||||
{
|
||||
@@ -573,7 +593,7 @@ function buildTemplateCountrySections(): PatrolDeleteCountrySection[] {
|
||||
}
|
||||
|
||||
function buildTemplateCartRatios(): PatrolDeleteCartRatio[] {
|
||||
return COUNTRY_TEMPLATE.map((country) => ({
|
||||
return selectedCountryNames.value.map((country) => ({
|
||||
country,
|
||||
ratio: "",
|
||||
}));
|
||||
@@ -745,6 +765,29 @@ function loadQueueState() {
|
||||
}
|
||||
}
|
||||
|
||||
function saveCountryCodes() {
|
||||
if (typeof window === "undefined") return;
|
||||
window.localStorage.setItem(
|
||||
countryCodesStorageKey(),
|
||||
JSON.stringify(selectedCountryCodes.value),
|
||||
);
|
||||
}
|
||||
|
||||
function loadCountryCodes() {
|
||||
try {
|
||||
const raw =
|
||||
typeof window !== "undefined"
|
||||
? window.localStorage.getItem(countryCodesStorageKey())
|
||||
: null;
|
||||
// 没存过(首次进入)时保持默认的五国全选,与改动前的行为一致
|
||||
selectedCountryCodes.value = raw
|
||||
? sanitizeCountryCodes(JSON.parse(raw))
|
||||
: [...EU_COUNTRY_CODES];
|
||||
} catch {
|
||||
selectedCountryCodes.value = [...EU_COUNTRY_CODES];
|
||||
}
|
||||
}
|
||||
|
||||
function clearActiveQueueTask() {
|
||||
activeTaskId.value = null;
|
||||
saveQueueState();
|
||||
@@ -1011,6 +1054,8 @@ function buildQueuePayload(taskId: number, items: PatrolDeleteHistoryItem[]) {
|
||||
source: "frontend-vue-patrol-delete",
|
||||
delete_conditions: deleteConditionsForTask,
|
||||
deleteConditions: deleteConditionsForTask,
|
||||
// 与店铺数据抓取 / 跟价 / 商品风险等页面统一:国家一律用 country_codes 传代码
|
||||
country_codes: [...selectedCountryCodes.value],
|
||||
items: items.map((item) => ({
|
||||
shopName: item.shopName,
|
||||
shopId: item.shopId,
|
||||
@@ -1122,10 +1167,11 @@ async function processQueue() {
|
||||
queuePayloadText.value = JSON.stringify(payload, null, 2);
|
||||
|
||||
// 删除条件为空时 Python 端没有可执行的判定规则,任务会一直停在执行中
|
||||
// country_codes 为空时 Python 端没有可遍历的站点,同样会空转
|
||||
const guard = checkQueuePayload(payload, {
|
||||
expectedType: "patrol-delete-run",
|
||||
requiredDataKeys: ["taskId"],
|
||||
nonEmptyArrayKeys: ["items", "delete_conditions"],
|
||||
nonEmptyArrayKeys: ["items", "delete_conditions", "country_codes"],
|
||||
});
|
||||
if (!(await passGuard(guard))) {
|
||||
await submitPatrolDeleteTaskResult(created.taskId, {
|
||||
@@ -1171,7 +1217,7 @@ async function processQueue() {
|
||||
}
|
||||
|
||||
for (const item of runnable) removeMatchedRowLocally(item);
|
||||
queuePushResult.value = `任务 ${created.taskId} 已入队,共 ${createdItems.length} 个店铺,等待执行完成`;
|
||||
queuePushResult.value = `任务 ${created.taskId} 已入队,共 ${createdItems.length} 个店铺、${selectedCountryNames.value.length} 个国家,等待执行完成`;
|
||||
|
||||
const finalStatus = await waitForTaskTerminal(created.taskId);
|
||||
queuePushResult.value = `任务 ${created.taskId} ${finalStatus === "SUCCESS" ? "已完成" : finalStatus === "DELETED" ? "已删除" : "执行失败"}`;
|
||||
@@ -1198,7 +1244,11 @@ async function pushToPythonQueue() {
|
||||
ElMessage.warning("请先匹配可用店铺");
|
||||
return;
|
||||
}
|
||||
queuePushResult.value = `开始创建任务,共 ${runnable.length} 个店铺`;
|
||||
if (!selectedCountryCodes.value.length) {
|
||||
ElMessage.warning("请至少勾选 1 个巡店国家");
|
||||
return;
|
||||
}
|
||||
queuePushResult.value = `开始创建任务,共 ${runnable.length} 个店铺、${selectedCountryNames.value.length} 个国家(${selectedCountryNames.value.join("、")})`;
|
||||
await processQueue();
|
||||
}
|
||||
|
||||
@@ -1250,6 +1300,8 @@ async function deleteTaskRecord(item: PatrolDeleteHistoryItem) {
|
||||
onMounted(async () => {
|
||||
loadMatchedItems();
|
||||
loadQueueState();
|
||||
loadCountryCodes();
|
||||
watch(selectedCountryCodes, saveCountryCodes, { deep: true });
|
||||
await Promise.all([loadCandidates(), loadConditions(), loadDashboard(), loadHistory()]);
|
||||
|
||||
if (activeTaskId.value) {
|
||||
|
||||
Reference in New Issue
Block a user