From 64e32523cdd557420c6f4ef55febf2e57ab028c9 Mon Sep 17 00:00:00 2001 From: super <2903208875@qq.com> Date: Sun, 22 Mar 2026 13:33:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=90=8E=E5=8F=B0=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/DedupeTotalDataController.java | 14 +- .../vo/DedupeTotalDataImportProgressVo.java | 30 ++++ .../vo/DedupeTotalDataImportStartVo.java | 12 ++ .../service/DedupeTotalDataService.java | 118 +++++++++++++++- .../__pycache__/admin_api.cpython-312.pyc | Bin 37649 -> 38536 bytes backend/blueprints/admin_api.py | 33 ++++- backend/config.py | 2 +- backend/web_source/admin.html | 129 +++++++++++++++--- 8 files changed, 303 insertions(+), 35 deletions(-) create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/vo/DedupeTotalDataImportProgressVo.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/vo/DedupeTotalDataImportStartVo.java diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/controller/DedupeTotalDataController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/controller/DedupeTotalDataController.java index 21df710..b451b9f 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/controller/DedupeTotalDataController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/controller/DedupeTotalDataController.java @@ -3,6 +3,8 @@ package com.nanri.aiimage.modules.dedupe.controller; import com.nanri.aiimage.common.api.ApiResponse; import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataCreateRequest; import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataUpdateRequest; +import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportProgressVo; +import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportStartVo; import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportVo; import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataItemVo; import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataPageVo; @@ -59,12 +61,18 @@ public class DedupeTotalDataController { @PostMapping("/import") @Operation(summary = "导入总数据", description = "上传 xlsx 文件,读取 ASIN 列并批量导入数据去重总数据。") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "导入成功", content = @Content(schema = @Schema(implementation = DedupeTotalDataImportVo.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "启动导入成功", content = @Content(schema = @Schema(implementation = DedupeTotalDataImportStartVo.class))), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "文件不合法或缺少 ASIN 列") }) - public ApiResponse importExcel( + public ApiResponse importExcel( @Parameter(description = "xlsx 文件", required = true) @RequestParam("file") MultipartFile file) { - return ApiResponse.success("导入成功", dedupeTotalDataService.importFromExcel(file)); + return ApiResponse.success("开始导入", dedupeTotalDataService.startImport(file)); + } + + @GetMapping("/import/{importId}") + @Operation(summary = "查询导入进度", description = "根据导入任务 ID 查询当前进度。") + public ApiResponse importProgress(@PathVariable String importId) { + return ApiResponse.success(dedupeTotalDataService.getImportProgress(importId)); } @PutMapping("/{id}") diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/vo/DedupeTotalDataImportProgressVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/vo/DedupeTotalDataImportProgressVo.java new file mode 100644 index 0000000..2753b51 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/vo/DedupeTotalDataImportProgressVo.java @@ -0,0 +1,30 @@ +package com.nanri.aiimage.modules.dedupe.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "数据去重总数据导入进度") +public class DedupeTotalDataImportProgressVo { + + @Schema(description = "任务状态:pending/running/success/failed") + private String status; + + @Schema(description = "总行数") + private Integer totalRows; + + @Schema(description = "已处理行数") + private Integer processedRows; + + @Schema(description = "读取到的 ASIN 数量") + private Integer asinCount; + + @Schema(description = "实际新增数量") + private Integer insertedCount; + + @Schema(description = "跳过数量") + private Integer skippedCount; + + @Schema(description = "错误信息") + private String errorMessage; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/vo/DedupeTotalDataImportStartVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/vo/DedupeTotalDataImportStartVo.java new file mode 100644 index 0000000..e0d543c --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/model/vo/DedupeTotalDataImportStartVo.java @@ -0,0 +1,12 @@ +package com.nanri.aiimage.modules.dedupe.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "数据去重总数据导入启动结果") +public class DedupeTotalDataImportStartVo { + + @Schema(description = "导入任务ID") + private String importId; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeTotalDataService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeTotalDataService.java index 3629af9..4387087 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeTotalDataService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeTotalDataService.java @@ -1,5 +1,6 @@ package com.nanri.aiimage.modules.dedupe.service; +import cn.hutool.core.util.IdUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.nanri.aiimage.common.exception.BusinessException; @@ -7,6 +8,8 @@ import com.nanri.aiimage.modules.dedupe.mapper.DedupeTotalDataMapper; import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataCreateRequest; import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataUpdateRequest; import com.nanri.aiimage.modules.dedupe.model.entity.DedupeTotalDataEntity; +import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportProgressVo; +import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportStartVo; import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportVo; import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataItemVo; import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataPageVo; @@ -20,18 +23,21 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; @Service @RequiredArgsConstructor public class DedupeTotalDataService { private final DedupeTotalDataMapper dedupeTotalDataMapper; + private final Map importProgressMap = new ConcurrentHashMap<>(); public DedupeTotalDataPageVo page(long page, long pageSize, String keyword) { long safePage = Math.max(page, 1); @@ -63,19 +69,85 @@ public class DedupeTotalDataService { return toItemVo(getById(entity.getId())); } + public DedupeTotalDataImportStartVo startImport(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new BusinessException("请上传 xlsx 文件"); + } + String importId = IdUtil.fastSimpleUUID(); + DedupeTotalDataImportProgressVo progress = new DedupeTotalDataImportProgressVo(); + progress.setStatus("pending"); + progress.setTotalRows(0); + progress.setProcessedRows(0); + progress.setAsinCount(0); + progress.setInsertedCount(0); + progress.setSkippedCount(0); + importProgressMap.put(importId, progress); + + try { + byte[] fileBytes = file.getBytes(); + String filename = file.getOriginalFilename(); + Thread.ofVirtual().start(() -> runImportTask(importId, fileBytes, filename)); + } catch (Exception e) { + importProgressMap.remove(importId); + throw new BusinessException("读取上传文件失败"); + } + + DedupeTotalDataImportStartVo vo = new DedupeTotalDataImportStartVo(); + vo.setImportId(importId); + return vo; + } + + public DedupeTotalDataImportProgressVo getImportProgress(String importId) { + DedupeTotalDataImportProgressVo progress = importProgressMap.get(importId); + if (progress == null) { + throw new BusinessException("导入任务不存在"); + } + return progress; + } + + private void runImportTask(String importId, byte[] fileBytes, String filename) { + DedupeTotalDataImportProgressVo progress = importProgressMap.get(importId); + if (progress == null) { + return; + } + progress.setStatus("running"); + try (InputStream inputStream = new ByteArrayInputStream(fileBytes)) { + DedupeTotalDataImportVo result = importFromExcelInternal(inputStream, filename, progress); + progress.setTotalRows(result.getTotalRows()); + progress.setAsinCount(result.getAsinCount()); + progress.setInsertedCount(result.getInsertedCount()); + progress.setSkippedCount(result.getSkippedCount()); + progress.setProcessedRows(result.getTotalRows()); + progress.setStatus("success"); + } catch (Exception e) { + progress.setStatus("failed"); + progress.setErrorMessage(e instanceof BusinessException ? e.getMessage() : "导入 Excel 失败"); + } + } + @Transactional public DedupeTotalDataImportVo importFromExcel(MultipartFile file) { if (file == null || file.isEmpty()) { throw new BusinessException("请上传 xlsx 文件"); } - String filename = file.getOriginalFilename() == null ? "" : file.getOriginalFilename().toLowerCase(); - if (!(filename.endsWith(".xlsx") || filename.endsWith(".xls"))) { + try (InputStream inputStream = file.getInputStream()) { + return importFromExcelInternal(inputStream, file.getOriginalFilename(), null); + } catch (BusinessException e) { + throw e; + } catch (Exception e) { + throw new BusinessException("导入 Excel 失败"); + } + } + + @Transactional + private DedupeTotalDataImportVo importFromExcelInternal(InputStream inputStream, String filename, DedupeTotalDataImportProgressVo progress) { + String lowerFilename = filename == null ? "" : filename.toLowerCase(); + if (!(lowerFilename.endsWith(".xlsx") || lowerFilename.endsWith(".xls"))) { throw new BusinessException("仅支持 .xlsx 或 .xls 文件"); } DataFormatter formatter = new DataFormatter(); - try (InputStream inputStream = file.getInputStream(); - Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(inputStream)) { + try (Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(inputStream)) { Sheet sheet = workbook.getSheetAt(0); Row headerRow = sheet.getRow(0); if (headerRow == null) { @@ -101,32 +173,70 @@ public class DedupeTotalDataService { int asinCount = 0; int insertedCount = 0; int skippedCount = 0; + if (progress != null) { + progress.setTotalRows(totalRows); + progress.setProcessedRows(0); + progress.setAsinCount(0); + progress.setInsertedCount(0); + progress.setSkippedCount(0); + progress.setErrorMessage(null); + } for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) { Row row = sheet.getRow(rowNum); if (row == null) { skippedCount++; + if (progress != null) { + progress.setProcessedRows(rowNum); + progress.setAsinCount(asinCount); + progress.setInsertedCount(insertedCount); + progress.setSkippedCount(skippedCount); + } continue; } String asin = normalizeExcelText(formatter.formatCellValue(row.getCell(asinIndex))); if (asin.isBlank()) { skippedCount++; + if (progress != null) { + progress.setProcessedRows(rowNum); + progress.setAsinCount(asinCount); + progress.setInsertedCount(insertedCount); + progress.setSkippedCount(skippedCount); + } continue; } asinCount++; String dataValue = normalizeDataValue(asin); if (!seenInFile.add(dataValue)) { skippedCount++; + if (progress != null) { + progress.setProcessedRows(rowNum); + progress.setAsinCount(asinCount); + progress.setInsertedCount(insertedCount); + progress.setSkippedCount(skippedCount); + } continue; } if (existsDataValue(dataValue)) { skippedCount++; + if (progress != null) { + progress.setProcessedRows(rowNum); + progress.setAsinCount(asinCount); + progress.setInsertedCount(insertedCount); + progress.setSkippedCount(skippedCount); + } continue; } DedupeTotalDataEntity entity = new DedupeTotalDataEntity(); entity.setDataValue(dataValue); dedupeTotalDataMapper.insert(entity); insertedCount++; + if (progress != null) { + progress.setProcessedRows(rowNum); + progress.setAsinCount(asinCount); + progress.setInsertedCount(insertedCount); + progress.setSkippedCount(skippedCount); + } } DedupeTotalDataImportVo vo = new DedupeTotalDataImportVo(); diff --git a/backend/blueprints/__pycache__/admin_api.cpython-312.pyc b/backend/blueprints/__pycache__/admin_api.cpython-312.pyc index 2412fe2373970c68af87c6e76408345691e6ca45..3fbdcebd412958b17f3ea56734870a1e95f069f1 100644 GIT binary patch delta 1338 zcma)5O>7%Q6y90ydL93_{z>GGIGq2j>E z`)1~S-}~mxo9E&!df_A5{J><=Q+UkJ+>cLZism*@{lw>B=n6p3GXu5(zBL9uY^%_O zLs5d2A4Hd#UNp_@MPD!hUy$GT7#^CZDk`}-zj~;oX~k6cW7MJlpdLo;Y}rtrq^2~d zs7c^IB7M$^*>G1sFYuXhp)Z|EM-zQ~G#%}aC&yDT-G5Y_BXK@_7VMl%CL{hd_}jV$ z(7TrhnHlh!22H$aL-#u@_BeHcNwb7T2G7heD(GM6{7%Q3ciB9(U<`3 znC9nH8GSuE0x?xFHzU#5+*7lPsU8(cs$qT__7Hs?X2l@(`8Dtv?0|=fA*J)PN=vi$ ziWYM+i8TC)z$9h(2R0%h?k5Ab8M(K%cHp$zccKWZLJTTp6iKXVmVlI z2CB}`YK!dZEC-eX%javZLseIBbwc)d%Za7L%CVZ~NYyjEHt|0g?ymBgr86rBYVJ_g z9bOyhk^kj6wl=zH(K+;+tii6`q6|iEgEFuj^x!L?V6NN1;YFhx@4#WHl6wK{p>Zc@ zc6EUj&AzqZy_Z{mKfnI*z4eRlDLS<}$9Y_R@*MC2F@F-(8*||{xJ|ark}I5;NhYIk z8vQcYYiDtLl}H`G$E124?fXzx162Fbc3WKZ_FBNp95C;R&?}a`PBHaMV zRUkLU$3F3}@SB$ZBBUvg4kI-fPZ?A1Kz0@(fy^R9}v delta 710 zcmeBJ%QSHr6W?iGUM>b8I1;-*^FYx=J_*KI8`VWwI8wyZ8P`axW}0lrVk&G1NHe#V9#hu}66F_7-75kVBY(__F{L1H*KN5{Aj~Ju;KKibW^i)8*PM*6PN{$Uk{~ zr?{ZNYDS3R6^zA9j0`CdQ9*2?szT_ZDZ+DDQbgu3!^DFbG({)pwslCa-}AI}>9dXr zPrK&d;w;H8Nz4h#FE1_vx`ko#vL+2qO_p0+VByLCn+!xXnTxc5GDWpO;ud>xX>M*} zQRQUmc02J~Y(=TXr8y-CN8$}xmHE*#qgTjXmbJJfYrDhoBPXi} z*GG0v5w_0^oILCw7&te#bfhye{+j%z%a)Y^7*x|Y>vhX8GyP?l?9*qc@DJpo|3E^M z{T6#>NosCmMetsHs0xtLFLG6G2e(vr*9 diff --git a/backend/blueprints/admin_api.py b/backend/blueprints/admin_api.py index 51db218..3eec1d9 100644 --- a/backend/blueprints/admin_api.py +++ b/backend/blueprints/admin_api.py @@ -631,6 +631,30 @@ def list_dedupe_total_data(): }) +@admin_api.route('/dedupe-total-data/import/') +@admin_required +def dedupe_total_data_import_progress(import_id): + result, error_response, status = _proxy_backend_java( + 'GET', + f'/api/admin/dedupe-total-data/import/{import_id}', + ) + if error_response is not None: + return error_response, status + progress = result.get('data') or {} + return jsonify({ + 'success': True, + 'progress': { + 'status': progress.get('status') or 'pending', + 'total_rows': progress.get('totalRows') or 0, + 'processed_rows': progress.get('processedRows') or 0, + 'asin_count': progress.get('asinCount') or 0, + 'inserted_count': progress.get('insertedCount') or 0, + 'skipped_count': progress.get('skippedCount') or 0, + 'error_message': progress.get('errorMessage') or '', + }, + }) + + @admin_api.route('/dedupe-total-data/import', methods=['POST']) @admin_required def import_dedupe_total_data(): @@ -650,13 +674,8 @@ def import_dedupe_total_data(): summary = result.get('data') or {} return jsonify({ 'success': True, - 'msg': result.get('message') or '导入成功', - 'summary': { - 'total_rows': summary.get('totalRows') or 0, - 'asin_count': summary.get('asinCount') or 0, - 'inserted_count': summary.get('insertedCount') or 0, - 'skipped_count': summary.get('skippedCount') or 0, - }, + 'msg': result.get('message') or '开始导入', + 'import_id': summary.get('importId') or '', }) diff --git a/backend/config.py b/backend/config.py index f32d0ac..20612c2 100644 --- a/backend/config.py +++ b/backend/config.py @@ -23,7 +23,7 @@ bucket_path = "nanri-image/" file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/" import os -backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/') +backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/') os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6" diff --git a/backend/web_source/admin.html b/backend/web_source/admin.html index 2f6a304..3827770 100644 --- a/backend/web_source/admin.html +++ b/backend/web_source/admin.html @@ -61,6 +61,10 @@ .msg { margin-top: 12px; font-size: 14px; } .msg.ok { color: #27ae60; } .msg.err { color: #e74c3c; } + .progress-wrap { margin-top: 12px; } + .progress-bar { width: 100%; height: 10px; background: #edf0f5; border-radius: 999px; overflow: hidden; } + .progress-fill { width: 0; height: 100%; background: #667eea; transition: width 0.2s ease; } + .progress-text { margin-top: 8px; font-size: 13px; color: #666; } table { width: 100%; border-collapse: collapse; } th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; font-size: 14px; } @@ -256,6 +260,10 @@

+

总数据列表

@@ -459,12 +467,28 @@ bindUserActions(); updateCreateFormByRole(); updateUserFilterByRole(); + updateDedupeTotalDataAccess(); }) .catch(function() { document.getElementById('userListBody').innerHTML = '请求失败'; }); } - function updateUserFilterByRole() { + function updateDedupeTotalDataAccess() { + var tab = document.querySelector('.tab[data-tab="dedupe-total-data"]'); + var panel = document.getElementById('panel-dedupe-total-data'); + var canUse = currentUserRole === 'admin' || currentUserRole === 'super_admin'; + if (tab) tab.style.display = canUse ? '' : 'none'; + if (panel) panel.style.display = canUse ? '' : 'none'; + if (!canUse && tab && tab.classList.contains('active')) { + tab.classList.remove('active'); + if (panel) panel.classList.remove('active'); + var usersTab = document.querySelector('.tab[data-tab="users"]'); + var usersPanel = document.getElementById('panel-users'); + if (usersTab) usersTab.classList.add('active'); + if (usersPanel) usersPanel.classList.add('active'); + } + } + var grp = document.getElementById('filterCreatedByGroup'); var sel = document.getElementById('filterCreatedBy'); if (currentUserRole === 'super_admin') { @@ -843,11 +867,66 @@ }); } document.getElementById('btnSearchDedupeTotalData').onclick = function() { loadDedupeTotalData(1); }; + var dedupeImportPollTimer = null; + function stopDedupeImportProgress() { + if (dedupeImportPollTimer) { + clearInterval(dedupeImportPollTimer); + dedupeImportPollTimer = null; + } + } + function setDedupeImportProgress(percent, text) { + var wrap = document.getElementById('dedupeTotalDataProgressWrap'); + var fill = document.getElementById('dedupeTotalDataProgressFill'); + var textEl = document.getElementById('dedupeTotalDataProgressText'); + wrap.style.display = 'block'; + fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; + textEl.textContent = text || ''; + } + function pollDedupeImport(importId) { + stopDedupeImportProgress(); + function tick() { + fetch('/api/admin/dedupe-total-data/import/' + encodeURIComponent(importId)) + .then(function(r) { return r.json(); }) + .then(function(res) { + if (!res.success) { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = res.error || '查询导入进度失败'; + document.getElementById('msgDedupeTotalData').className = 'msg err'; + return; + } + var progress = res.progress || {}; + var totalRows = progress.total_rows || 0; + var processedRows = progress.processed_rows || 0; + var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; + setDedupeImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); + if (progress.status === 'success') { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = '导入成功:总行数 ' + (progress.total_rows || 0) + ',ASIN 数量 ' + (progress.asin_count || 0) + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); + document.getElementById('msgDedupeTotalData').className = 'msg ok'; + loadDedupeTotalData(1); + } else if (progress.status === 'failed') { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = progress.error_message || '导入失败'; + document.getElementById('msgDedupeTotalData').className = 'msg err'; + } + }) + .catch(function() { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = '查询导入进度失败'; + document.getElementById('msgDedupeTotalData').className = 'msg err'; + }); + } + tick(); + dedupeImportPollTimer = setInterval(tick, 1000); + } + document.getElementById('btnAddDedupeTotalData').onclick = function() { var fileInput = document.getElementById('dedupeTotalDataFile'); var msgEl = document.getElementById('msgDedupeTotalData'); msgEl.textContent = ''; msgEl.className = 'msg'; + stopDedupeImportProgress(); + document.getElementById('dedupeTotalDataProgressWrap').style.display = 'none'; if (!fileInput.files || fileInput.files.length === 0) { msgEl.textContent = '请选择 Excel 文件'; msgEl.classList.add('err'); @@ -862,27 +941,37 @@ } var formData = new FormData(); formData.append('file', file); - fetch('/api/admin/dedupe-total-data/import', { - method: 'POST', - body: formData - }) - .then(function(r) { return r.json(); }) - .then(function(res) { - if (res.success) { - var summary = res.summary || {}; - msgEl.textContent = '导入成功:总行数 ' + (summary.total_rows || 0) + ',ASIN 数量 ' + (summary.asin_count || 0) + ',新增 ' + (summary.inserted_count || 0) + ',跳过 ' + (summary.skipped_count || 0); - msgEl.classList.add('ok'); - fileInput.value = ''; - loadDedupeTotalData(1); - } else { - msgEl.textContent = res.error || '导入失败'; - msgEl.classList.add('err'); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/api/admin/dedupe-total-data/import', true); + xhr.upload.onprogress = function(event) { + if (event.lengthComputable) { + var percent = Math.round(event.loaded * 100 / event.total); + setDedupeImportProgress(percent, '上传中:' + percent + '%'); } - }) - .catch(function() { + }; + xhr.onreadystatechange = function() { + if (xhr.readyState !== 4) return; + if (xhr.status < 200 || xhr.status >= 300) { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + return; + } + var res; + try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; } + if (!res.success) { + msgEl.textContent = res.error || '导入失败'; + msgEl.className = 'msg err'; + return; + } + setDedupeImportProgress(100, '上传完成,后端处理中...'); + pollDedupeImport(res.import_id); + fileInput.value = ''; + }; + xhr.onerror = function() { msgEl.textContent = '请求失败'; - msgEl.classList.add('err'); - }); + msgEl.className = 'msg err'; + }; + xhr.send(formData); }; document.getElementById('btnSaveDedupeTotalData').onclick = function() { var itemId = document.getElementById('editDedupeTotalDataId').value;