完成后台管理开发
This commit is contained in:
@@ -3,6 +3,8 @@ package com.nanri.aiimage.modules.dedupe.controller;
|
|||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataCreateRequest;
|
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.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.DedupeTotalDataImportVo;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataItemVo;
|
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataItemVo;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataPageVo;
|
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataPageVo;
|
||||||
@@ -59,12 +61,18 @@ public class DedupeTotalDataController {
|
|||||||
@PostMapping("/import")
|
@PostMapping("/import")
|
||||||
@Operation(summary = "导入总数据", description = "上传 xlsx 文件,读取 ASIN 列并批量导入数据去重总数据。")
|
@Operation(summary = "导入总数据", description = "上传 xlsx 文件,读取 ASIN 列并批量导入数据去重总数据。")
|
||||||
@ApiResponses({
|
@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 列")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "文件不合法或缺少 ASIN 列")
|
||||||
})
|
})
|
||||||
public ApiResponse<DedupeTotalDataImportVo> importExcel(
|
public ApiResponse<DedupeTotalDataImportStartVo> importExcel(
|
||||||
@Parameter(description = "xlsx 文件", required = true) @RequestParam("file") MultipartFile file) {
|
@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<DedupeTotalDataImportProgressVo> importProgress(@PathVariable String importId) {
|
||||||
|
return ApiResponse.success(dedupeTotalDataService.getImportProgress(importId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.nanri.aiimage.modules.dedupe.service;
|
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.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.nanri.aiimage.common.exception.BusinessException;
|
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.DedupeTotalDataCreateRequest;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataUpdateRequest;
|
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.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.DedupeTotalDataImportVo;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataItemVo;
|
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataItemVo;
|
||||||
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataPageVo;
|
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.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DedupeTotalDataService {
|
public class DedupeTotalDataService {
|
||||||
|
|
||||||
private final DedupeTotalDataMapper dedupeTotalDataMapper;
|
private final DedupeTotalDataMapper dedupeTotalDataMapper;
|
||||||
|
private final Map<String, DedupeTotalDataImportProgressVo> importProgressMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public DedupeTotalDataPageVo page(long page, long pageSize, String keyword) {
|
public DedupeTotalDataPageVo page(long page, long pageSize, String keyword) {
|
||||||
long safePage = Math.max(page, 1);
|
long safePage = Math.max(page, 1);
|
||||||
@@ -63,19 +69,85 @@ public class DedupeTotalDataService {
|
|||||||
return toItemVo(getById(entity.getId()));
|
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
|
@Transactional
|
||||||
public DedupeTotalDataImportVo importFromExcel(MultipartFile file) {
|
public DedupeTotalDataImportVo importFromExcel(MultipartFile file) {
|
||||||
if (file == null || file.isEmpty()) {
|
if (file == null || file.isEmpty()) {
|
||||||
throw new BusinessException("请上传 xlsx 文件");
|
throw new BusinessException("请上传 xlsx 文件");
|
||||||
}
|
}
|
||||||
String filename = file.getOriginalFilename() == null ? "" : file.getOriginalFilename().toLowerCase();
|
try (InputStream inputStream = file.getInputStream()) {
|
||||||
if (!(filename.endsWith(".xlsx") || filename.endsWith(".xls"))) {
|
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 文件");
|
throw new BusinessException("仅支持 .xlsx 或 .xls 文件");
|
||||||
}
|
}
|
||||||
|
|
||||||
DataFormatter formatter = new DataFormatter();
|
DataFormatter formatter = new DataFormatter();
|
||||||
try (InputStream inputStream = file.getInputStream();
|
try (Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(inputStream)) {
|
||||||
Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(inputStream)) {
|
|
||||||
Sheet sheet = workbook.getSheetAt(0);
|
Sheet sheet = workbook.getSheetAt(0);
|
||||||
Row headerRow = sheet.getRow(0);
|
Row headerRow = sheet.getRow(0);
|
||||||
if (headerRow == null) {
|
if (headerRow == null) {
|
||||||
@@ -101,32 +173,70 @@ public class DedupeTotalDataService {
|
|||||||
int asinCount = 0;
|
int asinCount = 0;
|
||||||
int insertedCount = 0;
|
int insertedCount = 0;
|
||||||
int skippedCount = 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++) {
|
for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
|
||||||
Row row = sheet.getRow(rowNum);
|
Row row = sheet.getRow(rowNum);
|
||||||
if (row == null) {
|
if (row == null) {
|
||||||
skippedCount++;
|
skippedCount++;
|
||||||
|
if (progress != null) {
|
||||||
|
progress.setProcessedRows(rowNum);
|
||||||
|
progress.setAsinCount(asinCount);
|
||||||
|
progress.setInsertedCount(insertedCount);
|
||||||
|
progress.setSkippedCount(skippedCount);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String asin = normalizeExcelText(formatter.formatCellValue(row.getCell(asinIndex)));
|
String asin = normalizeExcelText(formatter.formatCellValue(row.getCell(asinIndex)));
|
||||||
if (asin.isBlank()) {
|
if (asin.isBlank()) {
|
||||||
skippedCount++;
|
skippedCount++;
|
||||||
|
if (progress != null) {
|
||||||
|
progress.setProcessedRows(rowNum);
|
||||||
|
progress.setAsinCount(asinCount);
|
||||||
|
progress.setInsertedCount(insertedCount);
|
||||||
|
progress.setSkippedCount(skippedCount);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
asinCount++;
|
asinCount++;
|
||||||
String dataValue = normalizeDataValue(asin);
|
String dataValue = normalizeDataValue(asin);
|
||||||
if (!seenInFile.add(dataValue)) {
|
if (!seenInFile.add(dataValue)) {
|
||||||
skippedCount++;
|
skippedCount++;
|
||||||
|
if (progress != null) {
|
||||||
|
progress.setProcessedRows(rowNum);
|
||||||
|
progress.setAsinCount(asinCount);
|
||||||
|
progress.setInsertedCount(insertedCount);
|
||||||
|
progress.setSkippedCount(skippedCount);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (existsDataValue(dataValue)) {
|
if (existsDataValue(dataValue)) {
|
||||||
skippedCount++;
|
skippedCount++;
|
||||||
|
if (progress != null) {
|
||||||
|
progress.setProcessedRows(rowNum);
|
||||||
|
progress.setAsinCount(asinCount);
|
||||||
|
progress.setInsertedCount(insertedCount);
|
||||||
|
progress.setSkippedCount(skippedCount);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
DedupeTotalDataEntity entity = new DedupeTotalDataEntity();
|
DedupeTotalDataEntity entity = new DedupeTotalDataEntity();
|
||||||
entity.setDataValue(dataValue);
|
entity.setDataValue(dataValue);
|
||||||
dedupeTotalDataMapper.insert(entity);
|
dedupeTotalDataMapper.insert(entity);
|
||||||
insertedCount++;
|
insertedCount++;
|
||||||
|
if (progress != null) {
|
||||||
|
progress.setProcessedRows(rowNum);
|
||||||
|
progress.setAsinCount(asinCount);
|
||||||
|
progress.setInsertedCount(insertedCount);
|
||||||
|
progress.setSkippedCount(skippedCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DedupeTotalDataImportVo vo = new DedupeTotalDataImportVo();
|
DedupeTotalDataImportVo vo = new DedupeTotalDataImportVo();
|
||||||
|
|||||||
Binary file not shown.
@@ -631,6 +631,30 @@ def list_dedupe_total_data():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_api.route('/dedupe-total-data/import/<import_id>')
|
||||||
|
@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_api.route('/dedupe-total-data/import', methods=['POST'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def import_dedupe_total_data():
|
def import_dedupe_total_data():
|
||||||
@@ -650,13 +674,8 @@ def import_dedupe_total_data():
|
|||||||
summary = result.get('data') or {}
|
summary = result.get('data') or {}
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'msg': result.get('message') or '导入成功',
|
'msg': result.get('message') or '开始导入',
|
||||||
'summary': {
|
'import_id': summary.get('importId') or '',
|
||||||
'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,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ bucket_path = "nanri-image/"
|
|||||||
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
|
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
|
||||||
|
|
||||||
import os
|
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_ID'] = accessKeyId
|
||||||
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
|
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
|
||||||
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
||||||
|
|||||||
@@ -61,6 +61,10 @@
|
|||||||
.msg { margin-top: 12px; font-size: 14px; }
|
.msg { margin-top: 12px; font-size: 14px; }
|
||||||
.msg.ok { color: #27ae60; }
|
.msg.ok { color: #27ae60; }
|
||||||
.msg.err { color: #e74c3c; }
|
.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; }
|
table { width: 100%; border-collapse: collapse; }
|
||||||
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; font-size: 14px; }
|
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; font-size: 14px; }
|
||||||
@@ -256,6 +260,10 @@
|
|||||||
<button class="btn" id="btnAddDedupeTotalData">上传并导入</button>
|
<button class="btn" id="btnAddDedupeTotalData">上传并导入</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="msg" id="msgDedupeTotalData"></p>
|
<p class="msg" id="msgDedupeTotalData"></p>
|
||||||
|
<div class="progress-wrap" id="dedupeTotalDataProgressWrap" style="display:none;">
|
||||||
|
<div class="progress-bar"><div class="progress-fill" id="dedupeTotalDataProgressFill"></div></div>
|
||||||
|
<div class="progress-text" id="dedupeTotalDataProgressText"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-box">
|
<div class="panel-box">
|
||||||
<h3 style="margin-bottom:16px;font-size:15px;">总数据列表</h3>
|
<h3 style="margin-bottom:16px;font-size:15px;">总数据列表</h3>
|
||||||
@@ -459,12 +467,28 @@
|
|||||||
bindUserActions();
|
bindUserActions();
|
||||||
updateCreateFormByRole();
|
updateCreateFormByRole();
|
||||||
updateUserFilterByRole();
|
updateUserFilterByRole();
|
||||||
|
updateDedupeTotalDataAccess();
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
document.getElementById('userListBody').innerHTML = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
|
document.getElementById('userListBody').innerHTML = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
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 grp = document.getElementById('filterCreatedByGroup');
|
||||||
var sel = document.getElementById('filterCreatedBy');
|
var sel = document.getElementById('filterCreatedBy');
|
||||||
if (currentUserRole === 'super_admin') {
|
if (currentUserRole === 'super_admin') {
|
||||||
@@ -843,11 +867,66 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
document.getElementById('btnSearchDedupeTotalData').onclick = function() { loadDedupeTotalData(1); };
|
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() {
|
document.getElementById('btnAddDedupeTotalData').onclick = function() {
|
||||||
var fileInput = document.getElementById('dedupeTotalDataFile');
|
var fileInput = document.getElementById('dedupeTotalDataFile');
|
||||||
var msgEl = document.getElementById('msgDedupeTotalData');
|
var msgEl = document.getElementById('msgDedupeTotalData');
|
||||||
msgEl.textContent = '';
|
msgEl.textContent = '';
|
||||||
msgEl.className = 'msg';
|
msgEl.className = 'msg';
|
||||||
|
stopDedupeImportProgress();
|
||||||
|
document.getElementById('dedupeTotalDataProgressWrap').style.display = 'none';
|
||||||
if (!fileInput.files || fileInput.files.length === 0) {
|
if (!fileInput.files || fileInput.files.length === 0) {
|
||||||
msgEl.textContent = '请选择 Excel 文件';
|
msgEl.textContent = '请选择 Excel 文件';
|
||||||
msgEl.classList.add('err');
|
msgEl.classList.add('err');
|
||||||
@@ -862,27 +941,37 @@
|
|||||||
}
|
}
|
||||||
var formData = new FormData();
|
var formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
fetch('/api/admin/dedupe-total-data/import', {
|
var xhr = new XMLHttpRequest();
|
||||||
method: 'POST',
|
xhr.open('POST', '/api/admin/dedupe-total-data/import', true);
|
||||||
body: formData
|
xhr.upload.onprogress = function(event) {
|
||||||
})
|
if (event.lengthComputable) {
|
||||||
.then(function(r) { return r.json(); })
|
var percent = Math.round(event.loaded * 100 / event.total);
|
||||||
.then(function(res) {
|
setDedupeImportProgress(percent, '上传中:' + percent + '%');
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
})
|
};
|
||||||
.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.textContent = '请求失败';
|
||||||
msgEl.classList.add('err');
|
msgEl.className = 'msg err';
|
||||||
});
|
};
|
||||||
|
xhr.send(formData);
|
||||||
};
|
};
|
||||||
document.getElementById('btnSaveDedupeTotalData').onclick = function() {
|
document.getElementById('btnSaveDedupeTotalData').onclick = function() {
|
||||||
var itemId = document.getElementById('editDedupeTotalDataId').value;
|
var itemId = document.getElementById('editDedupeTotalDataId').value;
|
||||||
|
|||||||
Reference in New Issue
Block a user