修复商品风险多店铺问题
This commit is contained in:
4
app/.env
4
app/.env
@@ -11,8 +11,8 @@ zn_username=%E8%87%AA%E5%8A%A8%E5%8C%96_Robot
|
|||||||
client_name=ShuFuAI
|
client_name=ShuFuAI
|
||||||
|
|
||||||
|
|
||||||
java_api_base=http://47.111.163.154:18080
|
# java_api_base=http://47.111.163.154:18080
|
||||||
# java_api_base=http://127.0.0.1:18080
|
java_api_base=http://127.0.0.1:18080
|
||||||
# java_api_base=http://8.136.19.173:18080
|
# java_api_base=http://8.136.19.173:18080
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -195,10 +195,13 @@ def proxy(path):
|
|||||||
if key.lower() in ['host', 'content-length', 'connection']:
|
if key.lower() in ['host', 'content-length', 'connection']:
|
||||||
continue
|
continue
|
||||||
headers[key] = value
|
headers[key] = value
|
||||||
ignore_url = [f"{JAVA_API_BASE}/api/delete-brand/tasks/batch",
|
ignore_url = [f"{JAVA_API_BASE}/api/delete-brand/tasks/batch",
|
||||||
f"{JAVA_API_BASE}/api/delete-brand/history",
|
f"{JAVA_API_BASE}/api/delete-brand/tasks/progress/batch",
|
||||||
f"{JAVA_API_BASE}/api/product-risk-resolve/tasks/batch",
|
f"{JAVA_API_BASE}/api/delete-brand/history",
|
||||||
]
|
f"{JAVA_API_BASE}/api/product-risk-resolve/tasks/batch",
|
||||||
|
f"{JAVA_API_BASE}/api/product-risk-resolve/tasks/progress/batch",
|
||||||
|
]
|
||||||
|
proxy_timeout = 180 if target_url.endswith("/api/delete-brand/run") else 30
|
||||||
if target_url not in ignore_url:
|
if target_url not in ignore_url:
|
||||||
try:
|
try:
|
||||||
print("=============================")
|
print("=============================")
|
||||||
@@ -219,7 +222,7 @@ def proxy(path):
|
|||||||
data=request.get_data() if request.get_data() else None,
|
data=request.get_data() if request.get_data() else None,
|
||||||
cookies=request.cookies,
|
cookies=request.cookies,
|
||||||
stream=True, # 启用流式传输
|
stream=True, # 启用流式传输
|
||||||
timeout=30
|
timeout=proxy_timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
# 流式响应
|
# 流式响应
|
||||||
|
|||||||
@@ -12,4 +12,8 @@ public class TransientStorageProperties {
|
|||||||
private String accessKeyId;
|
private String accessKeyId;
|
||||||
private String accessKeySecret;
|
private String accessKeySecret;
|
||||||
private String region = "us-east-1";
|
private String region = "us-east-1";
|
||||||
|
private int connectTimeoutSeconds = 10;
|
||||||
|
private int readTimeoutSeconds = 60;
|
||||||
|
private int writeTimeoutSeconds = 60;
|
||||||
|
private int uploadMaxRetries = 3;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,7 +240,10 @@ public class DeleteBrandTaskStorageService {
|
|||||||
DeleteBrandResultFileDto dto = objectMapper.readValue(payloadJson, DeleteBrandResultFileDto.class);
|
DeleteBrandResultFileDto dto = objectMapper.readValue(payloadJson, DeleteBrandResultFileDto.class);
|
||||||
grouped.computeIfAbsent(entity.getScopeKey(), ignored -> new ArrayList<>()).add(dto);
|
grouped.computeIfAbsent(entity.getScopeKey(), ignored -> new ArrayList<>()).add(dto);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new BusinessException("failed to load delete-brand result chunks");
|
log.warn("[delete-brand-storage] failed to load result chunk taskId={} scopeKey={} chunkIndex={} payload={}",
|
||||||
|
taskId, entity.getScopeKey(), entity.getChunkIndex(), entity.getPayloadJson(), ex);
|
||||||
|
throw new BusinessException("failed to load delete-brand result chunks: taskId="
|
||||||
|
+ taskId + ", scopeKey=" + entity.getScopeKey() + ", chunkIndex=" + entity.getChunkIndex());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (List<DeleteBrandResultFileDto> chunks : grouped.values()) {
|
for (List<DeleteBrandResultFileDto> chunks : grouped.values()) {
|
||||||
|
|||||||
@@ -5,18 +5,24 @@ import io.minio.GetObjectArgs;
|
|||||||
import io.minio.MinioClient;
|
import io.minio.MinioClient;
|
||||||
import io.minio.PutObjectArgs;
|
import io.minio.PutObjectArgs;
|
||||||
import io.minio.RemoveObjectArgs;
|
import io.minio.RemoveObjectArgs;
|
||||||
|
import io.minio.StatObjectArgs;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class RustfsObjectStorageService {
|
public class RustfsObjectStorageService {
|
||||||
|
|
||||||
private final TransientStorageProperties properties;
|
private final TransientStorageProperties properties;
|
||||||
|
private volatile OkHttpClient httpClient;
|
||||||
|
|
||||||
public boolean isConfigured() {
|
public boolean isConfigured() {
|
||||||
return notBlank(properties.getEndpoint())
|
return notBlank(properties.getEndpoint())
|
||||||
@@ -29,18 +35,28 @@ public class RustfsObjectStorageService {
|
|||||||
if (!isConfigured()) {
|
if (!isConfigured()) {
|
||||||
throw new IllegalStateException("transient storage is not configured");
|
throw new IllegalStateException("transient storage is not configured");
|
||||||
}
|
}
|
||||||
try (ByteArrayInputStream stream = new ByteArrayInputStream(
|
byte[] bytes = Objects.requireNonNullElse(content, "").getBytes(StandardCharsets.UTF_8);
|
||||||
Objects.requireNonNullElse(content, "").getBytes(StandardCharsets.UTF_8))) {
|
Exception last = null;
|
||||||
buildClient().putObject(PutObjectArgs.builder()
|
int maxRetries = Math.max(1, properties.getUploadMaxRetries());
|
||||||
.bucket(properties.getBucket())
|
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
.object(objectKey)
|
try (ByteArrayInputStream stream = new ByteArrayInputStream(bytes)) {
|
||||||
.stream(stream, stream.available(), -1)
|
buildClient().putObject(PutObjectArgs.builder()
|
||||||
.contentType("application/json")
|
.bucket(properties.getBucket())
|
||||||
.build());
|
.object(objectKey)
|
||||||
return objectKey;
|
.stream(stream, bytes.length, -1)
|
||||||
} catch (Exception ex) {
|
.contentType("application/json")
|
||||||
throw new IllegalStateException("failed to upload payload to transient storage", ex);
|
.build());
|
||||||
|
verifyObjectVisible(objectKey);
|
||||||
|
return objectKey;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
last = ex;
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
log.warn("[rustfs] upload failed, retrying objectKey={} attempt={}/{}", objectKey, attempt, maxRetries, ex);
|
||||||
|
sleepQuietly(500L * attempt);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
throw new IllegalStateException("failed to upload payload to transient storage", last);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String readObjectAsString(String objectKey) {
|
public String readObjectAsString(String objectKey) {
|
||||||
@@ -76,9 +92,57 @@ public class RustfsObjectStorageService {
|
|||||||
.endpoint(properties.getEndpoint())
|
.endpoint(properties.getEndpoint())
|
||||||
.credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
|
.credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
|
||||||
.region(properties.getRegion())
|
.region(properties.getRegion())
|
||||||
|
.httpClient(getHttpClient())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private OkHttpClient getHttpClient() {
|
||||||
|
OkHttpClient current = httpClient;
|
||||||
|
if (current != null) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
if (httpClient == null) {
|
||||||
|
httpClient = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(Math.max(1, properties.getConnectTimeoutSeconds()), TimeUnit.SECONDS)
|
||||||
|
.readTimeout(Math.max(1, properties.getReadTimeoutSeconds()), TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(Math.max(1, properties.getWriteTimeoutSeconds()), TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
return httpClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyObjectVisible(String objectKey) {
|
||||||
|
RuntimeException last = null;
|
||||||
|
for (int attempt = 1; attempt <= 3; attempt++) {
|
||||||
|
try {
|
||||||
|
buildClient().statObject(StatObjectArgs.builder()
|
||||||
|
.bucket(properties.getBucket())
|
||||||
|
.object(objectKey)
|
||||||
|
.build());
|
||||||
|
return;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
last = new IllegalStateException("transient payload is not visible after upload: " + objectKey, ex);
|
||||||
|
if (attempt < 3) {
|
||||||
|
sleepQuietly(100L * attempt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw last == null
|
||||||
|
? new IllegalStateException("transient payload is not visible after upload: " + objectKey)
|
||||||
|
: last;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sleepQuietly(long millis) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(millis);
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException("interrupted while verifying transient payload upload", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean notBlank(String value) {
|
private boolean notBlank(String value) {
|
||||||
return value != null && !value.isBlank();
|
return value != null && !value.isBlank();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.nanri.aiimage.modules.shopkey.controller;
|
|||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCountryUpdateRequest;
|
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCountryUpdateRequest;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest;
|
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinImportProgressVo;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinImportStartVo;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinItemVo;
|
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinItemVo;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinPageVo;
|
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinPageVo;
|
||||||
import com.nanri.aiimage.modules.shopkey.service.SkipPriceAsinService;
|
import com.nanri.aiimage.modules.shopkey.service.SkipPriceAsinService;
|
||||||
@@ -20,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -59,6 +62,40 @@ public class SkipPriceAsinController {
|
|||||||
skipPriceAsinService.createOrUpdate(request, operatorId, Boolean.TRUE.equals(superAdmin)));
|
skipPriceAsinService.createOrUpdate(request, operatorId, Boolean.TRUE.equals(superAdmin)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/import")
|
||||||
|
@Operation(summary = "导入新增跳过跟价 ASIN")
|
||||||
|
public ApiResponse<QueryAsinImportStartVo> importExcel(
|
||||||
|
@Parameter(description = "xlsx/xls 文件,文件名必须等于店铺名", required = true) @RequestParam("file") MultipartFile file,
|
||||||
|
@Parameter(description = "页面选择的分组ID", required = true) @RequestParam Long groupId,
|
||||||
|
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
||||||
|
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
||||||
|
return ApiResponse.success("开始导入",
|
||||||
|
skipPriceAsinService.startImport(file, groupId, operatorId, Boolean.TRUE.equals(superAdmin)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/import/{importId}")
|
||||||
|
@Operation(summary = "查询导入新增进度")
|
||||||
|
public ApiResponse<QueryAsinImportProgressVo> importProgress(@PathVariable String importId) {
|
||||||
|
return ApiResponse.success(skipPriceAsinService.getImportProgress(importId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/delete-import")
|
||||||
|
@Operation(summary = "导入删除跳过跟价 ASIN")
|
||||||
|
public ApiResponse<QueryAsinImportStartVo> deleteImportExcel(
|
||||||
|
@Parameter(description = "xlsx/xls 文件,文件名必须等于店铺名", required = true) @RequestParam("file") MultipartFile file,
|
||||||
|
@Parameter(description = "页面选择的分组ID", required = true) @RequestParam Long groupId,
|
||||||
|
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
||||||
|
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
||||||
|
return ApiResponse.success("开始删除",
|
||||||
|
skipPriceAsinService.startDeleteImport(file, groupId, operatorId, Boolean.TRUE.equals(superAdmin)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/delete-import/{importId}")
|
||||||
|
@Operation(summary = "查询导入删除进度")
|
||||||
|
public ApiResponse<QueryAsinImportProgressVo> deleteImportProgress(@PathVariable String importId) {
|
||||||
|
return ApiResponse.success(skipPriceAsinService.getDeleteImportProgress(importId));
|
||||||
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}/countries/{country}")
|
@PutMapping("/{id}/countries/{country}")
|
||||||
@Operation(summary = "编辑指定国家的跳过跟价 ASIN")
|
@Operation(summary = "编辑指定国家的跳过跟价 ASIN")
|
||||||
public ApiResponse<SkipPriceAsinItemVo> updateCountry(
|
public ApiResponse<SkipPriceAsinItemVo> updateCountry(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.nanri.aiimage.modules.shopkey.model.entity;
|
package com.nanri.aiimage.modules.shopkey.model.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
@@ -25,31 +26,31 @@ public class SkipPriceAsinEntity {
|
|||||||
@TableField("asin_de")
|
@TableField("asin_de")
|
||||||
private String asinDe;
|
private String asinDe;
|
||||||
|
|
||||||
@TableField("minimum_price_de")
|
@TableField(value = "minimum_price_de", updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private BigDecimal minimumPriceDe;
|
private BigDecimal minimumPriceDe;
|
||||||
|
|
||||||
@TableField("asin_uk")
|
@TableField("asin_uk")
|
||||||
private String asinUk;
|
private String asinUk;
|
||||||
|
|
||||||
@TableField("minimum_price_uk")
|
@TableField(value = "minimum_price_uk", updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private BigDecimal minimumPriceUk;
|
private BigDecimal minimumPriceUk;
|
||||||
|
|
||||||
@TableField("asin_fr")
|
@TableField("asin_fr")
|
||||||
private String asinFr;
|
private String asinFr;
|
||||||
|
|
||||||
@TableField("minimum_price_fr")
|
@TableField(value = "minimum_price_fr", updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private BigDecimal minimumPriceFr;
|
private BigDecimal minimumPriceFr;
|
||||||
|
|
||||||
@TableField("asin_it")
|
@TableField("asin_it")
|
||||||
private String asinIt;
|
private String asinIt;
|
||||||
|
|
||||||
@TableField("minimum_price_it")
|
@TableField(value = "minimum_price_it", updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private BigDecimal minimumPriceIt;
|
private BigDecimal minimumPriceIt;
|
||||||
|
|
||||||
@TableField("asin_es")
|
@TableField("asin_es")
|
||||||
private String asinEs;
|
private String asinEs;
|
||||||
|
|
||||||
@TableField("minimum_price_es")
|
@TableField(value = "minimum_price_es", updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private BigDecimal minimumPriceEs;
|
private BigDecimal minimumPriceEs;
|
||||||
|
|
||||||
@TableField("created_at")
|
@TableField("created_at")
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
package com.nanri.aiimage.modules.shopkey.service;
|
package com.nanri.aiimage.modules.shopkey.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import cn.hutool.core.util.IdUtil;
|
||||||
import com.nanri.aiimage.common.exception.BusinessException;
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
import com.nanri.aiimage.modules.shopkey.mapper.SkipPriceAsinMapper;
|
import com.nanri.aiimage.modules.shopkey.mapper.SkipPriceAsinMapper;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.mapper.ShopManageMapper;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest;
|
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageEntity;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity;
|
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.entity.SkipPriceAsinEntity;
|
import com.nanri.aiimage.modules.shopkey.model.entity.SkipPriceAsinEntity;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinImportProgressVo;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinImportStartVo;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinItemVo;
|
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinItemVo;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinPageVo;
|
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinPageVo;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.poi.ss.usermodel.Cell;
|
||||||
|
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||||
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -21,15 +37,20 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
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
|
||||||
|
@Slf4j
|
||||||
public class SkipPriceAsinService {
|
public class SkipPriceAsinService {
|
||||||
|
|
||||||
private static final List<String> SUPPORTED_COUNTRIES = List.of("DE", "UK", "FR", "IT", "ES");
|
private static final List<String> SUPPORTED_COUNTRIES = List.of("DE", "UK", "FR", "IT", "ES");
|
||||||
|
|
||||||
private final SkipPriceAsinMapper skipPriceAsinMapper;
|
private final SkipPriceAsinMapper skipPriceAsinMapper;
|
||||||
|
private final ShopManageMapper shopManageMapper;
|
||||||
private final ShopManageGroupService shopManageGroupService;
|
private final ShopManageGroupService shopManageGroupService;
|
||||||
|
private final Map<String, QueryAsinImportProgressVo> importProgressMap = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, QueryAsinImportProgressVo> deleteImportProgressMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public SkipPriceAsinPageVo page(long page, long pageSize, Long groupId, String shopName, String asin,
|
public SkipPriceAsinPageVo page(long page, long pageSize, Long groupId, String shopName, String asin,
|
||||||
Long operatorId, boolean superAdmin) {
|
Long operatorId, boolean superAdmin) {
|
||||||
@@ -141,6 +162,486 @@ public class SkipPriceAsinService {
|
|||||||
return toItemVo(saved, groupName);
|
return toItemVo(saved, groupName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public QueryAsinImportStartVo startImport(MultipartFile file, Long groupId, Long operatorId, boolean superAdmin) {
|
||||||
|
return startImportTask(file, groupId, operatorId, superAdmin, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryAsinImportProgressVo getImportProgress(String importId) {
|
||||||
|
QueryAsinImportProgressVo progress = importProgressMap.get(importId);
|
||||||
|
if (progress == null) {
|
||||||
|
throw new BusinessException("导入任务不存在");
|
||||||
|
}
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryAsinImportStartVo startDeleteImport(MultipartFile file, Long groupId, Long operatorId, boolean superAdmin) {
|
||||||
|
return startImportTask(file, groupId, operatorId, superAdmin, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryAsinImportProgressVo getDeleteImportProgress(String importId) {
|
||||||
|
QueryAsinImportProgressVo progress = deleteImportProgressMap.get(importId);
|
||||||
|
if (progress == null) {
|
||||||
|
throw new BusinessException("删除任务不存在");
|
||||||
|
}
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private QueryAsinImportStartVo startImportTask(MultipartFile file, Long groupId, Long operatorId,
|
||||||
|
boolean superAdmin, boolean deleteMode) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new BusinessException("请上传 xlsx 或 xls 文件");
|
||||||
|
}
|
||||||
|
validateExcelFilename(file.getOriginalFilename());
|
||||||
|
if (groupId == null || groupId <= 0) {
|
||||||
|
throw new BusinessException("请先选择分组");
|
||||||
|
}
|
||||||
|
shopManageGroupService.getAccessibleById(groupId, operatorId, superAdmin);
|
||||||
|
String shopName = normalizeShopNameFromFilename(file.getOriginalFilename());
|
||||||
|
ensureShopExistsInGroup(groupId, shopName);
|
||||||
|
|
||||||
|
String importId = IdUtil.fastSimpleUUID();
|
||||||
|
QueryAsinImportProgressVo progress = newImportProgress();
|
||||||
|
Map<String, QueryAsinImportProgressVo> progressMap = deleteMode ? deleteImportProgressMap : importProgressMap;
|
||||||
|
progressMap.put(importId, progress);
|
||||||
|
try {
|
||||||
|
File tempFile = saveMultipartToTempFile(file, deleteMode ? "skip-price-asin-delete-" : "skip-price-asin-import-");
|
||||||
|
Thread.ofVirtual().start(() -> runImportTask(importId, tempFile, file.getOriginalFilename(), groupId,
|
||||||
|
shopName, deleteMode));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
progressMap.remove(importId);
|
||||||
|
throw new BusinessException("读取上传文件失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryAsinImportStartVo vo = new QueryAsinImportStartVo();
|
||||||
|
vo.setImportId(importId);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private QueryAsinImportProgressVo newImportProgress() {
|
||||||
|
QueryAsinImportProgressVo progress = new QueryAsinImportProgressVo();
|
||||||
|
progress.setStatus("pending");
|
||||||
|
progress.setTotalRows(0);
|
||||||
|
progress.setProcessedRows(0);
|
||||||
|
progress.setAsinCount(0);
|
||||||
|
progress.setInsertedCount(0);
|
||||||
|
progress.setDeletedCount(0);
|
||||||
|
progress.setSkippedCount(0);
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runImportTask(String importId, File tempFile, String filename, Long groupId,
|
||||||
|
String shopName, boolean deleteMode) {
|
||||||
|
Map<String, QueryAsinImportProgressVo> progressMap = deleteMode ? deleteImportProgressMap : importProgressMap;
|
||||||
|
QueryAsinImportProgressVo progress = progressMap.get(importId);
|
||||||
|
if (progress == null) {
|
||||||
|
deleteQuietly(tempFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
progress.setStatus("running");
|
||||||
|
progress.setErrorMessage(null);
|
||||||
|
try {
|
||||||
|
processImportFile(tempFile, filename, groupId, shopName, deleteMode, progress);
|
||||||
|
progress.setStatus("success");
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("[skip-price-asin-import] failed importId={} deleteMode={} msg={}",
|
||||||
|
importId, deleteMode, ex.getMessage());
|
||||||
|
progress.setStatus("failed");
|
||||||
|
progress.setErrorMessage(ex instanceof BusinessException ? ex.getMessage()
|
||||||
|
: (deleteMode ? "导入删除 Excel 失败" : "导入新增 Excel 失败"));
|
||||||
|
} finally {
|
||||||
|
deleteQuietly(tempFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processImportFile(File tempFile, String filename, Long groupId, String shopName,
|
||||||
|
boolean deleteMode, QueryAsinImportProgressVo progress) throws Exception {
|
||||||
|
validateExcelFilename(filename);
|
||||||
|
try (FileInputStream inputStream = new FileInputStream(tempFile);
|
||||||
|
Workbook workbook = WorkbookFactory.create(inputStream)) {
|
||||||
|
Sheet sheet = workbook.getNumberOfSheets() == 0 ? null : workbook.getSheetAt(0);
|
||||||
|
if (sheet == null) {
|
||||||
|
throw new BusinessException("Excel 为空");
|
||||||
|
}
|
||||||
|
HeaderMapping mapping = resolveHeaderMapping(sheet);
|
||||||
|
if (mapping.asinColumns().isEmpty()) {
|
||||||
|
throw new BusinessException("Excel 至少需要包含一个国家的 ASIN 列");
|
||||||
|
}
|
||||||
|
|
||||||
|
int firstDataRow = mapping.firstDataRowIndex();
|
||||||
|
int lastRow = Math.max(sheet.getLastRowNum(), firstDataRow - 1);
|
||||||
|
int totalRows = Math.max(0, lastRow - firstDataRow + 1);
|
||||||
|
progress.setTotalRows(totalRows);
|
||||||
|
|
||||||
|
DataFormatter formatter = new DataFormatter();
|
||||||
|
ImportCounters counters = new ImportCounters();
|
||||||
|
for (int rowIndex = firstDataRow; rowIndex <= lastRow; rowIndex++) {
|
||||||
|
Row row = sheet.getRow(rowIndex);
|
||||||
|
if (row == null || isBlankRow(row, formatter)) {
|
||||||
|
counters.skippedCount++;
|
||||||
|
updateImportProgress(progress, counters, rowIndex - firstDataRow + 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (deleteMode) {
|
||||||
|
consumeDeleteImportRow(row, formatter, mapping, groupId, shopName, counters);
|
||||||
|
} else {
|
||||||
|
consumeAddImportRow(row, formatter, mapping, groupId, shopName, counters);
|
||||||
|
}
|
||||||
|
updateImportProgress(progress, counters, rowIndex - firstDataRow + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumeAddImportRow(Row row, DataFormatter formatter, HeaderMapping mapping,
|
||||||
|
Long groupId, String shopName, ImportCounters counters) {
|
||||||
|
boolean acceptedAny = false;
|
||||||
|
boolean sawAnyAsin = false;
|
||||||
|
SkipPriceAsinEntity entity = new SkipPriceAsinEntity();
|
||||||
|
entity.setGroupId(groupId);
|
||||||
|
entity.setShopName(shopName);
|
||||||
|
for (Map.Entry<String, Integer> entry : mapping.asinColumns().entrySet()) {
|
||||||
|
String country = entry.getKey();
|
||||||
|
String asin = normalizeBlank(cellText(row, entry.getValue(), formatter)).toUpperCase(Locale.ROOT);
|
||||||
|
if (asin.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sawAnyAsin = true;
|
||||||
|
counters.asinCount++;
|
||||||
|
if (asin.length() > 64) {
|
||||||
|
counters.skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal minimumPrice = parseOptionalMinimumPrice(
|
||||||
|
cellText(row, mapping.priceColumns().get(country), formatter));
|
||||||
|
if (existsCountryAsin(groupId, shopName, country, asin)) {
|
||||||
|
counters.skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
setCountryData(entity, country, asin, minimumPrice);
|
||||||
|
counters.insertedCount++;
|
||||||
|
acceptedAny = true;
|
||||||
|
}
|
||||||
|
if (!acceptedAny) {
|
||||||
|
if (!sawAnyAsin) {
|
||||||
|
counters.skippedCount++;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
skipPriceAsinMapper.insert(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumeDeleteImportRow(Row row, DataFormatter formatter, HeaderMapping mapping,
|
||||||
|
Long groupId, String shopName, ImportCounters counters) {
|
||||||
|
boolean acceptedAny = false;
|
||||||
|
boolean sawAnyAsin = false;
|
||||||
|
for (Map.Entry<String, Integer> entry : mapping.asinColumns().entrySet()) {
|
||||||
|
String country = entry.getKey();
|
||||||
|
String asin = normalizeBlank(cellText(row, entry.getValue(), formatter)).toUpperCase(Locale.ROOT);
|
||||||
|
if (asin.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sawAnyAsin = true;
|
||||||
|
counters.asinCount++;
|
||||||
|
if (asin.length() > 64) {
|
||||||
|
counters.skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SkipPriceAsinEntity entity = findCountryAsin(groupId, shopName, country, asin);
|
||||||
|
if (entity == null) {
|
||||||
|
counters.skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
setCountryData(entity, country, "", null);
|
||||||
|
if (isEmptyRow(entity)) {
|
||||||
|
skipPriceAsinMapper.deleteById(entity.getId());
|
||||||
|
} else {
|
||||||
|
skipPriceAsinMapper.updateById(entity);
|
||||||
|
}
|
||||||
|
counters.deletedCount++;
|
||||||
|
acceptedAny = true;
|
||||||
|
}
|
||||||
|
if (!acceptedAny && !sawAnyAsin) {
|
||||||
|
counters.skippedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean existsCountryAsin(Long groupId, String shopName, String country, String asin) {
|
||||||
|
return findCountryAsin(groupId, shopName, country, asin) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SkipPriceAsinEntity findCountryAsin(Long groupId, String shopName, String country, String asin) {
|
||||||
|
LambdaQueryWrapper<SkipPriceAsinEntity> query = new LambdaQueryWrapper<SkipPriceAsinEntity>()
|
||||||
|
.eq(SkipPriceAsinEntity::getGroupId, groupId)
|
||||||
|
.eq(SkipPriceAsinEntity::getShopName, shopName);
|
||||||
|
switch (country) {
|
||||||
|
case "DE" -> query.eq(SkipPriceAsinEntity::getAsinDe, asin);
|
||||||
|
case "UK" -> query.eq(SkipPriceAsinEntity::getAsinUk, asin);
|
||||||
|
case "FR" -> query.eq(SkipPriceAsinEntity::getAsinFr, asin);
|
||||||
|
case "IT" -> query.eq(SkipPriceAsinEntity::getAsinIt, asin);
|
||||||
|
case "ES" -> query.eq(SkipPriceAsinEntity::getAsinEs, asin);
|
||||||
|
default -> throw new BusinessException("鍥藉鍙傛暟涓嶆敮鎸? " + country);
|
||||||
|
}
|
||||||
|
return skipPriceAsinMapper.selectOne(query.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private HeaderMapping resolveHeaderMapping(Sheet sheet) {
|
||||||
|
DataFormatter formatter = new DataFormatter();
|
||||||
|
Row firstHeader = sheet.getRow(0);
|
||||||
|
Row secondHeader = sheet.getRow(1);
|
||||||
|
if (firstHeader == null) {
|
||||||
|
throw new BusinessException("Excel 表头为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxColumn = Math.max(lastCellNum(firstHeader), lastCellNum(secondHeader));
|
||||||
|
Map<String, Integer> asinColumns = new LinkedHashMap<>();
|
||||||
|
Map<String, Integer> priceColumns = new LinkedHashMap<>();
|
||||||
|
for (int columnIndex = 0; columnIndex < maxColumn; columnIndex++) {
|
||||||
|
String countryText = cellTextWithMerged(sheet, firstHeader, columnIndex, formatter);
|
||||||
|
String fieldText = cellText(secondHeader, columnIndex, formatter);
|
||||||
|
String country = normalizeCountryAlias(countryText);
|
||||||
|
String fieldKey = normalizeHeaderKey(fieldText);
|
||||||
|
if (country.isEmpty()) {
|
||||||
|
country = normalizeCountryAlias(fieldText);
|
||||||
|
fieldKey = normalizeHeaderKey(countryText);
|
||||||
|
}
|
||||||
|
if (country.isEmpty() || !SUPPORTED_COUNTRIES.contains(country)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isAsinHeader(fieldKey)) {
|
||||||
|
asinColumns.putIfAbsent(country, columnIndex);
|
||||||
|
} else if (isPriceHeader(fieldKey)) {
|
||||||
|
priceColumns.putIfAbsent(country, columnIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (asinColumns.isEmpty()) {
|
||||||
|
for (int columnIndex = 0; columnIndex < maxColumn; columnIndex++) {
|
||||||
|
String headerText = cellText(firstHeader, columnIndex, formatter);
|
||||||
|
String key = normalizeHeaderKey(headerText);
|
||||||
|
String country = normalizeCountryAlias(headerText);
|
||||||
|
if (country.isEmpty()) {
|
||||||
|
country = countryFromHeaderKey(key);
|
||||||
|
}
|
||||||
|
if (!country.isEmpty() && isAsinHeader(key)) {
|
||||||
|
asinColumns.putIfAbsent(country, columnIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new HeaderMapping(asinColumns, priceColumns, 1);
|
||||||
|
}
|
||||||
|
return new HeaderMapping(asinColumns, priceColumns, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int lastCellNum(Row row) {
|
||||||
|
return row == null || row.getLastCellNum() < 0 ? 0 : row.getLastCellNum();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAsinHeader(String key) {
|
||||||
|
return key.contains("asin");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPriceHeader(String key) {
|
||||||
|
return key.contains("最低价") || key.contains("最低价格") || key.contains("minimumprice")
|
||||||
|
|| key.contains("minprice") || key.equals("price");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cellTextWithMerged(Sheet sheet, Row row, int columnIndex, DataFormatter formatter) {
|
||||||
|
if (row == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
int rowIndex = row.getRowNum();
|
||||||
|
for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
|
||||||
|
var region = sheet.getMergedRegion(i);
|
||||||
|
if (region.isInRange(rowIndex, columnIndex)) {
|
||||||
|
Row firstRow = sheet.getRow(region.getFirstRow());
|
||||||
|
return cellText(firstRow, region.getFirstColumn(), formatter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cellText(row, columnIndex, formatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cellText(Row row, Integer columnIndex, DataFormatter formatter) {
|
||||||
|
if (row == null || columnIndex == null || columnIndex < 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
Cell cell = row.getCell(columnIndex);
|
||||||
|
return cell == null ? "" : normalizeExcelText(formatter.formatCellValue(cell));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeExcelText(String value) {
|
||||||
|
return value == null ? "" : value.replace(String.valueOf((char) 0xFEFF), "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeHeaderKey(String value) {
|
||||||
|
return normalizeExcelText(value)
|
||||||
|
.toLowerCase(Locale.ROOT)
|
||||||
|
.replace(" ", "")
|
||||||
|
.replace("_", "")
|
||||||
|
.replace("-", "")
|
||||||
|
.replace("/", "")
|
||||||
|
.replace("\\", "")
|
||||||
|
.replace(":", "")
|
||||||
|
.replace(":", "")
|
||||||
|
.replace("(", "")
|
||||||
|
.replace(")", "")
|
||||||
|
.replace("(", "")
|
||||||
|
.replace(")", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeCountryAlias(String country) {
|
||||||
|
String normalized = normalizeBlank(country);
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String upper = normalized.toUpperCase(Locale.ROOT);
|
||||||
|
if (SUPPORTED_COUNTRIES.contains(upper)) {
|
||||||
|
return upper;
|
||||||
|
}
|
||||||
|
return switch (normalizeHeaderKey(normalized)) {
|
||||||
|
case "德国", "德", "de", "germany", "german", "deutschland" -> "DE";
|
||||||
|
case "英国", "英", "uk", "gb", "greatbritain", "britain", "unitedkingdom" -> "UK";
|
||||||
|
case "法国", "法", "fr", "france", "french" -> "FR";
|
||||||
|
case "意大利", "意", "it", "italy", "italian" -> "IT";
|
||||||
|
case "西班牙", "西", "es", "spain", "spanish", "espana" -> "ES";
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String countryFromHeaderKey(String key) {
|
||||||
|
if (key.contains("asinde") || key.contains("deasin") || key.contains("德国")) return "DE";
|
||||||
|
if (key.contains("asinuk") || key.contains("ukasin") || key.contains("英国")) return "UK";
|
||||||
|
if (key.contains("asinfr") || key.contains("frasin") || key.contains("法国")) return "FR";
|
||||||
|
if (key.contains("asinit") || key.contains("itasin") || key.contains("意大利")) return "IT";
|
||||||
|
if (key.contains("asines") || key.contains("esasin") || key.contains("西班牙")) return "ES";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal parseOptionalMinimumPrice(String value) {
|
||||||
|
String normalized = normalizeBlank(value);
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return normalizeMinimumPrice(new BigDecimal(normalized.replace(",", "")));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new BusinessException("最低价格式不正确: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean priceEquals(BigDecimal left, BigDecimal right) {
|
||||||
|
if (left == null && right == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (left == null || right == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return left.compareTo(right) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBlankRow(Row row, DataFormatter formatter) {
|
||||||
|
if (row == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < lastCellNum(row); i++) {
|
||||||
|
if (!cellText(row, i, formatter).isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateImportProgress(QueryAsinImportProgressVo progress, ImportCounters counters, int processedRows) {
|
||||||
|
progress.setProcessedRows(processedRows);
|
||||||
|
progress.setAsinCount(counters.asinCount);
|
||||||
|
progress.setInsertedCount(counters.insertedCount);
|
||||||
|
progress.setDeletedCount(counters.deletedCount);
|
||||||
|
progress.setSkippedCount(counters.skippedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private File saveMultipartToTempFile(MultipartFile file, String prefix) throws Exception {
|
||||||
|
String suffix = ".xlsx";
|
||||||
|
String name = file.getOriginalFilename();
|
||||||
|
if (name != null) {
|
||||||
|
int idx = name.lastIndexOf('.');
|
||||||
|
if (idx >= 0) {
|
||||||
|
suffix = name.substring(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File tempFile = File.createTempFile(prefix, suffix);
|
||||||
|
file.transferTo(tempFile);
|
||||||
|
return tempFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteQuietly(File file) {
|
||||||
|
if (file == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(file.toPath());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateExcelFilename(String filename) {
|
||||||
|
String lowerFilename = filename == null ? "" : filename.toLowerCase(Locale.ROOT);
|
||||||
|
if (!(lowerFilename.endsWith(".xlsx") || lowerFilename.endsWith(".xls"))) {
|
||||||
|
throw new BusinessException("仅支持 .xlsx 或 .xls 文件");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeShopNameFromFilename(String filename) {
|
||||||
|
String normalized = normalizeBlank(filename);
|
||||||
|
int slashIndex = Math.max(normalized.lastIndexOf('/'), normalized.lastIndexOf('\\'));
|
||||||
|
if (slashIndex >= 0) {
|
||||||
|
normalized = normalized.substring(slashIndex + 1);
|
||||||
|
}
|
||||||
|
int dotIndex = normalized.lastIndexOf('.');
|
||||||
|
if (dotIndex > 0) {
|
||||||
|
normalized = normalized.substring(0, dotIndex);
|
||||||
|
}
|
||||||
|
return normalizeRequired(normalized, "文件名必须是店铺名");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureShopExistsInGroup(Long groupId, String shopName) {
|
||||||
|
Long count = shopManageMapper.selectCount(new LambdaQueryWrapper<ShopManageEntity>()
|
||||||
|
.eq(ShopManageEntity::getGroupId, groupId)
|
||||||
|
.eq(ShopManageEntity::getShopName, shopName));
|
||||||
|
if (count == null || count <= 0) {
|
||||||
|
throw new BusinessException("当前分组下不存在文件名对应的店铺: " + shopName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCountryAsin(SkipPriceAsinEntity entity, String country) {
|
||||||
|
return switch (country) {
|
||||||
|
case "DE" -> normalizeBlank(entity.getAsinDe());
|
||||||
|
case "UK" -> normalizeBlank(entity.getAsinUk());
|
||||||
|
case "FR" -> normalizeBlank(entity.getAsinFr());
|
||||||
|
case "IT" -> normalizeBlank(entity.getAsinIt());
|
||||||
|
case "ES" -> normalizeBlank(entity.getAsinEs());
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal getCountryMinimumPrice(SkipPriceAsinEntity entity, String country) {
|
||||||
|
return switch (country) {
|
||||||
|
case "DE" -> entity.getMinimumPriceDe();
|
||||||
|
case "UK" -> entity.getMinimumPriceUk();
|
||||||
|
case "FR" -> entity.getMinimumPriceFr();
|
||||||
|
case "IT" -> entity.getMinimumPriceIt();
|
||||||
|
case "ES" -> entity.getMinimumPriceEs();
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private record HeaderMapping(Map<String, Integer> asinColumns,
|
||||||
|
Map<String, Integer> priceColumns,
|
||||||
|
int firstDataRowIndex) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ImportCounters {
|
||||||
|
private int asinCount;
|
||||||
|
private int insertedCount;
|
||||||
|
private int deletedCount;
|
||||||
|
private int skippedCount;
|
||||||
|
}
|
||||||
|
|
||||||
private SkipPriceAsinEntity getById(Long id) {
|
private SkipPriceAsinEntity getById(Long id) {
|
||||||
SkipPriceAsinEntity entity = skipPriceAsinMapper.selectById(id);
|
SkipPriceAsinEntity entity = skipPriceAsinMapper.selectById(id);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public class TransientPayloadStorageService {
|
|||||||
return ossStorageService.readObjectAsString(pointer.substring(OSS_POINTER_PREFIX.length()));
|
return ossStorageService.readObjectAsString(pointer.substring(OSS_POINTER_PREFIX.length()));
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new IllegalStateException(errorMessage, ex);
|
throw new IllegalStateException(errorMessage + ": " + pointer, ex);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ aiimage:
|
|||||||
access-key-id: ${AIIMAGE_TRANSIENT_STORAGE_ACCESS_KEY_ID:}
|
access-key-id: ${AIIMAGE_TRANSIENT_STORAGE_ACCESS_KEY_ID:}
|
||||||
access-key-secret: ${AIIMAGE_TRANSIENT_STORAGE_ACCESS_KEY_SECRET:}
|
access-key-secret: ${AIIMAGE_TRANSIENT_STORAGE_ACCESS_KEY_SECRET:}
|
||||||
region: ${AIIMAGE_TRANSIENT_STORAGE_REGION:us-east-1}
|
region: ${AIIMAGE_TRANSIENT_STORAGE_REGION:us-east-1}
|
||||||
|
connect-timeout-seconds: ${AIIMAGE_TRANSIENT_STORAGE_CONNECT_TIMEOUT_SECONDS:10}
|
||||||
|
read-timeout-seconds: ${AIIMAGE_TRANSIENT_STORAGE_READ_TIMEOUT_SECONDS:60}
|
||||||
|
write-timeout-seconds: ${AIIMAGE_TRANSIENT_STORAGE_WRITE_TIMEOUT_SECONDS:60}
|
||||||
|
upload-max-retries: ${AIIMAGE_TRANSIENT_STORAGE_UPLOAD_MAX_RETRIES:3}
|
||||||
storage:
|
storage:
|
||||||
local-temp-dir: ${AIIMAGE_STORAGE_LOCAL_TEMP_DIR:./data/tmp}
|
local-temp-dir: ${AIIMAGE_STORAGE_LOCAL_TEMP_DIR:./data/tmp}
|
||||||
cleanup-enabled: ${AIIMAGE_STORAGE_CLEANUP_ENABLED:true}
|
cleanup-enabled: ${AIIMAGE_STORAGE_CLEANUP_ENABLED:true}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
SET @db_name = DATABASE();
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_skip_price_asin'
|
||||||
|
AND INDEX_NAME = 'uk_group_shop_name'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists > 0,
|
||||||
|
'ALTER TABLE biz_skip_price_asin DROP INDEX uk_group_shop_name',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_skip_price_asin'
|
||||||
|
AND INDEX_NAME = 'idx_group_shop_name'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE biz_skip_price_asin ADD INDEX idx_group_shop_name (group_id, shop_name)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_skip_price_asin'
|
||||||
|
AND INDEX_NAME = 'idx_group_shop_asin_de'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE biz_skip_price_asin ADD INDEX idx_group_shop_asin_de (group_id, shop_name, asin_de)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_skip_price_asin'
|
||||||
|
AND INDEX_NAME = 'idx_group_shop_asin_uk'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE biz_skip_price_asin ADD INDEX idx_group_shop_asin_uk (group_id, shop_name, asin_uk)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_skip_price_asin'
|
||||||
|
AND INDEX_NAME = 'idx_group_shop_asin_fr'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE biz_skip_price_asin ADD INDEX idx_group_shop_asin_fr (group_id, shop_name, asin_fr)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_skip_price_asin'
|
||||||
|
AND INDEX_NAME = 'idx_group_shop_asin_it'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE biz_skip_price_asin ADD INDEX idx_group_shop_asin_it (group_id, shop_name, asin_it)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db_name
|
||||||
|
AND TABLE_NAME = 'biz_skip_price_asin'
|
||||||
|
AND INDEX_NAME = 'idx_group_shop_asin_es'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE biz_skip_price_asin ADD INDEX idx_group_shop_asin_es (group_id, shop_name, asin_es)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -1990,6 +1990,116 @@ def update_skip_price_asin_country(item_id, country):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_api.route('/skip-price-asins/import/<import_id>')
|
||||||
|
@login_required
|
||||||
|
def skip_price_asin_import_progress(import_id):
|
||||||
|
role, current_row, denied = _ensure_backend_menu_access('skip-price-asin')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
|
result, error_response, status = _proxy_backend_java(
|
||||||
|
'GET',
|
||||||
|
f'/api/admin/skip-price-asins/import/{import_id}',
|
||||||
|
)
|
||||||
|
if error_response is not None:
|
||||||
|
return error_response, status
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'progress': _format_query_asin_import_progress(result.get('data') or {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_api.route('/skip-price-asins/import', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def import_skip_price_asins():
|
||||||
|
role, current_row, denied = _ensure_backend_menu_access('skip-price-asin')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
|
file_storage = request.files.get('file')
|
||||||
|
if not file_storage or file_storage.filename == '':
|
||||||
|
return jsonify({'success': False, 'error': '请选择 Excel 文件'})
|
||||||
|
group_id_raw = (request.form.get('group_id') or '').strip()
|
||||||
|
if not group_id_raw:
|
||||||
|
return jsonify({'success': False, 'error': '请先选择分组'})
|
||||||
|
params = {
|
||||||
|
'groupId': group_id_raw,
|
||||||
|
'operatorId': current_row.get('id') if current_row else None,
|
||||||
|
'superAdmin': 'true' if role == 'super_admin' else 'false',
|
||||||
|
}
|
||||||
|
files = {
|
||||||
|
'file': (file_storage.filename, file_storage.stream, file_storage.mimetype or 'application/octet-stream')
|
||||||
|
}
|
||||||
|
result, error_response, status = _proxy_backend_java(
|
||||||
|
'POST',
|
||||||
|
'/api/admin/skip-price-asins/import',
|
||||||
|
params=params,
|
||||||
|
files=files,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
if error_response is not None:
|
||||||
|
return error_response, status
|
||||||
|
summary = result.get('data') or {}
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'msg': result.get('message') or '开始导入',
|
||||||
|
'import_id': summary.get('importId') or '',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_api.route('/skip-price-asins/delete-import/<import_id>')
|
||||||
|
@login_required
|
||||||
|
def skip_price_asin_delete_import_progress(import_id):
|
||||||
|
role, current_row, denied = _ensure_backend_menu_access('skip-price-asin')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
|
result, error_response, status = _proxy_backend_java(
|
||||||
|
'GET',
|
||||||
|
f'/api/admin/skip-price-asins/delete-import/{import_id}',
|
||||||
|
)
|
||||||
|
if error_response is not None:
|
||||||
|
return error_response, status
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'progress': _format_query_asin_import_progress(result.get('data') or {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_api.route('/skip-price-asins/delete-import', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def delete_import_skip_price_asins():
|
||||||
|
role, current_row, denied = _ensure_backend_menu_access('skip-price-asin')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
|
file_storage = request.files.get('file')
|
||||||
|
if not file_storage or file_storage.filename == '':
|
||||||
|
return jsonify({'success': False, 'error': '请选择 Excel 文件'})
|
||||||
|
group_id_raw = (request.form.get('group_id') or '').strip()
|
||||||
|
if not group_id_raw:
|
||||||
|
return jsonify({'success': False, 'error': '请先选择分组'})
|
||||||
|
params = {
|
||||||
|
'groupId': group_id_raw,
|
||||||
|
'operatorId': current_row.get('id') if current_row else None,
|
||||||
|
'superAdmin': 'true' if role == 'super_admin' else 'false',
|
||||||
|
}
|
||||||
|
files = {
|
||||||
|
'file': (file_storage.filename, file_storage.stream, file_storage.mimetype or 'application/octet-stream')
|
||||||
|
}
|
||||||
|
result, error_response, status = _proxy_backend_java(
|
||||||
|
'POST',
|
||||||
|
'/api/admin/skip-price-asins/delete-import',
|
||||||
|
params=params,
|
||||||
|
files=files,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
if error_response is not None:
|
||||||
|
return error_response, status
|
||||||
|
summary = result.get('data') or {}
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'msg': result.get('message') or '开始删除',
|
||||||
|
'import_id': summary.get('importId') or '',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def _format_query_asin_item(item):
|
def _format_query_asin_item(item):
|
||||||
return {
|
return {
|
||||||
'id': item.get('id'),
|
'id': item.get('id'),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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://127.0.0.1:18080').rstrip('/')
|
||||||
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
|
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
|
||||||
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://47.111.163.154:18080').rstrip('/')
|
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://47.111.163.154:18080').rstrip('/')
|
||||||
|
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://121.196.149.225: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"
|
||||||
|
|||||||
@@ -227,6 +227,65 @@
|
|||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.request-loading-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: #667eea;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 3000;
|
||||||
|
transition: width 0.2s ease, opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-loading-bar.show {
|
||||||
|
width: 74%;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-loading-bar.finishing {
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-loading {
|
||||||
|
position: fixed;
|
||||||
|
right: 24px;
|
||||||
|
top: 72px;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
color: #333;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
border: 1px solid #e6eaf2;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
font-size: 13px;
|
||||||
|
z-index: 3000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-loading.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-spinner {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid #dce2ff;
|
||||||
|
border-top-color: #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: request-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes request-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -661,6 +720,11 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- <div class="nav"><a href="/home">返回首页</a></div> -->
|
<!-- <div class="nav"><a href="/home">返回首页</a></div> -->
|
||||||
|
<div class="request-loading-bar" id="requestLoadingBar"></div>
|
||||||
|
<div class="request-loading" id="requestLoading">
|
||||||
|
<span class="request-spinner"></span>
|
||||||
|
<span>请求处理中...</span>
|
||||||
|
</div>
|
||||||
<h1>管理后台</h1>
|
<h1>管理后台</h1>
|
||||||
|
|
||||||
<div class="admin-user-box" style="position:absolute;top:24px;right:24px;z-index:10;">
|
<div class="admin-user-box" style="position:absolute;top:24px;right:24px;z-index:10;">
|
||||||
@@ -1042,6 +1106,42 @@
|
|||||||
<button class="btn" id="btnCreateSkipPriceAsin">新增 ASIN</button>
|
<button class="btn" id="btnCreateSkipPriceAsin">新增 ASIN</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="msg" id="msgSkipPriceAsin"></p>
|
<p class="msg" id="msgSkipPriceAsin"></p>
|
||||||
|
<div class="form-row" style="align-items:flex-start;gap:16px;margin-top:18px;border-top:1px solid #f0f0f0;padding-top:16px;">
|
||||||
|
<div style="flex:1;min-width:320px;">
|
||||||
|
<h3 style="margin-bottom:12px;font-size:14px;">导入文件新增</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group" style="min-width:320px;">
|
||||||
|
<label>上传 Excel(先选择分组;文件名必须是该分组下的店铺名;表头为 国家 / 删除ASIN / 最低价)</label>
|
||||||
|
<input type="file" id="skipPriceAsinImportFile" accept=".xlsx,.xls">
|
||||||
|
</div>
|
||||||
|
<button class="btn" id="btnImportSkipPriceAsin" type="button">上传并新增</button>
|
||||||
|
</div>
|
||||||
|
<p class="msg" id="msgSkipPriceAsinImport"></p>
|
||||||
|
<div class="progress-wrap" id="skipPriceAsinImportProgressWrap" style="display:none;">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="skipPriceAsinImportProgressFill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text" id="skipPriceAsinImportProgressText"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;min-width:320px;">
|
||||||
|
<h3 style="margin-bottom:12px;font-size:14px;">导入文件删除</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group" style="min-width:320px;">
|
||||||
|
<label>上传 Excel(先选择分组;文件名必须是该分组下的店铺名;按各国家删除ASIN列匹配删除)</label>
|
||||||
|
<input type="file" id="skipPriceAsinDeleteImportFile" accept=".xlsx,.xls">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-danger" id="btnDeleteImportSkipPriceAsin" type="button">上传并删除</button>
|
||||||
|
</div>
|
||||||
|
<p class="msg" id="msgSkipPriceAsinDeleteImport"></p>
|
||||||
|
<div class="progress-wrap" id="skipPriceAsinDeleteImportProgressWrap" style="display:none;">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="skipPriceAsinDeleteImportProgressFill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text" id="skipPriceAsinDeleteImportProgressText"></div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
@@ -1578,6 +1678,100 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
// 全局请求动画:拦截本页所有 fetch 和 XMLHttpRequest 请求。
|
||||||
|
(function installRequestLoadingInterceptor() {
|
||||||
|
if (window.__adminRequestLoadingInstalled) return;
|
||||||
|
window.__adminRequestLoadingInstalled = true;
|
||||||
|
var activeRequests = 0;
|
||||||
|
var showTimer = null;
|
||||||
|
var hideTimer = null;
|
||||||
|
var showDelayMs = 180;
|
||||||
|
var loadingEl = document.getElementById('requestLoading');
|
||||||
|
var loadingBarEl = document.getElementById('requestLoadingBar');
|
||||||
|
|
||||||
|
function showRequestLoading() {
|
||||||
|
if (hideTimer) {
|
||||||
|
clearTimeout(hideTimer);
|
||||||
|
hideTimer = null;
|
||||||
|
}
|
||||||
|
if (!loadingEl || !loadingBarEl) return;
|
||||||
|
loadingBarEl.classList.remove('finishing');
|
||||||
|
loadingBarEl.classList.add('show');
|
||||||
|
loadingEl.classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideRequestLoading() {
|
||||||
|
if (showTimer) {
|
||||||
|
clearTimeout(showTimer);
|
||||||
|
showTimer = null;
|
||||||
|
}
|
||||||
|
if (!loadingEl || !loadingBarEl) return;
|
||||||
|
loadingBarEl.classList.remove('show');
|
||||||
|
loadingBarEl.classList.add('finishing');
|
||||||
|
loadingEl.classList.remove('show');
|
||||||
|
hideTimer = setTimeout(function () {
|
||||||
|
loadingBarEl.classList.remove('finishing');
|
||||||
|
}, 220);
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginRequest() {
|
||||||
|
activeRequests += 1;
|
||||||
|
if (activeRequests === 1) {
|
||||||
|
showTimer = setTimeout(showRequestLoading, showDelayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function endRequest() {
|
||||||
|
activeRequests = Math.max(0, activeRequests - 1);
|
||||||
|
if (activeRequests === 0) {
|
||||||
|
hideRequestLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.fetch) {
|
||||||
|
var nativeFetch = window.fetch.bind(window);
|
||||||
|
window.fetch = function (input, init) {
|
||||||
|
var options = init || {};
|
||||||
|
var skipLoading = !!options.__skipLoading;
|
||||||
|
if (skipLoading) {
|
||||||
|
options = Object.assign({}, options);
|
||||||
|
delete options.__skipLoading;
|
||||||
|
} else {
|
||||||
|
beginRequest();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return nativeFetch(input, options).finally(function () {
|
||||||
|
if (!skipLoading) endRequest();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (!skipLoading) endRequest();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.XMLHttpRequest) {
|
||||||
|
var nativeOpen = XMLHttpRequest.prototype.open;
|
||||||
|
var nativeSend = XMLHttpRequest.prototype.send;
|
||||||
|
XMLHttpRequest.prototype.open = function () {
|
||||||
|
this.__adminSkipLoading = false;
|
||||||
|
return nativeOpen.apply(this, arguments);
|
||||||
|
};
|
||||||
|
XMLHttpRequest.prototype.send = function () {
|
||||||
|
if (!this.__adminSkipLoading) {
|
||||||
|
beginRequest();
|
||||||
|
this.addEventListener('loadend', endRequest, { once: true });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return nativeSend.apply(this, arguments);
|
||||||
|
} catch (err) {
|
||||||
|
if (!this.__adminSkipLoading) endRequest();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// Tab 切换
|
// Tab 切换
|
||||||
var adminTabsEl = document.getElementById('adminTabs');
|
var adminTabsEl = document.getElementById('adminTabs');
|
||||||
if (adminTabsEl) adminTabsEl.innerHTML = '';
|
if (adminTabsEl) adminTabsEl.innerHTML = '';
|
||||||
@@ -3441,6 +3635,270 @@
|
|||||||
document.getElementById('skipPriceAsinListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
|
document.getElementById('skipPriceAsinListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
var skipPriceAsinImportPollTimer = null;
|
||||||
|
var skipPriceAsinDeleteImportPollTimer = null;
|
||||||
|
var skipPriceAsinImportPollSeq = 0;
|
||||||
|
var skipPriceAsinDeleteImportPollSeq = 0;
|
||||||
|
function stopSkipPriceAsinImportProgress() {
|
||||||
|
skipPriceAsinImportPollSeq += 1;
|
||||||
|
if (skipPriceAsinImportPollTimer) {
|
||||||
|
clearTimeout(skipPriceAsinImportPollTimer);
|
||||||
|
skipPriceAsinImportPollTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function stopSkipPriceAsinDeleteImportProgress() {
|
||||||
|
skipPriceAsinDeleteImportPollSeq += 1;
|
||||||
|
if (skipPriceAsinDeleteImportPollTimer) {
|
||||||
|
clearTimeout(skipPriceAsinDeleteImportPollTimer);
|
||||||
|
skipPriceAsinDeleteImportPollTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setSkipPriceAsinImportProgress(percent, text) {
|
||||||
|
var wrap = document.getElementById('skipPriceAsinImportProgressWrap');
|
||||||
|
var fill = document.getElementById('skipPriceAsinImportProgressFill');
|
||||||
|
var textEl = document.getElementById('skipPriceAsinImportProgressText');
|
||||||
|
wrap.style.display = 'block';
|
||||||
|
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
|
||||||
|
textEl.textContent = text || '';
|
||||||
|
}
|
||||||
|
function setSkipPriceAsinDeleteImportProgress(percent, text) {
|
||||||
|
var wrap = document.getElementById('skipPriceAsinDeleteImportProgressWrap');
|
||||||
|
var fill = document.getElementById('skipPriceAsinDeleteImportProgressFill');
|
||||||
|
var textEl = document.getElementById('skipPriceAsinDeleteImportProgressText');
|
||||||
|
wrap.style.display = 'block';
|
||||||
|
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
|
||||||
|
textEl.textContent = text || '';
|
||||||
|
}
|
||||||
|
function pollSkipPriceAsinImport(importId) {
|
||||||
|
stopSkipPriceAsinImportProgress();
|
||||||
|
var pollSeq = ++skipPriceAsinImportPollSeq;
|
||||||
|
var finished = false;
|
||||||
|
var inFlight = false;
|
||||||
|
var attempts = 0;
|
||||||
|
var maxAttempts = 300;
|
||||||
|
function isActive() {
|
||||||
|
return !finished && pollSeq === skipPriceAsinImportPollSeq;
|
||||||
|
}
|
||||||
|
function finish() {
|
||||||
|
finished = true;
|
||||||
|
if (skipPriceAsinImportPollTimer) {
|
||||||
|
clearTimeout(skipPriceAsinImportPollTimer);
|
||||||
|
skipPriceAsinImportPollTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function schedule() {
|
||||||
|
if (isActive()) {
|
||||||
|
skipPriceAsinImportPollTimer = setTimeout(tick, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function tick() {
|
||||||
|
if (!isActive() || inFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attempts += 1;
|
||||||
|
if (attempts > maxAttempts) {
|
||||||
|
finish();
|
||||||
|
document.getElementById('msgSkipPriceAsinImport').textContent = '查询导入进度超时,请稍后刷新列表确认结果';
|
||||||
|
document.getElementById('msgSkipPriceAsinImport').className = 'msg err';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inFlight = true;
|
||||||
|
fetch('/api/admin/skip-price-asins/import/' + encodeURIComponent(importId))
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (res) {
|
||||||
|
if (!isActive()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res.success) {
|
||||||
|
finish();
|
||||||
|
document.getElementById('msgSkipPriceAsinImport').textContent = res.error || '查询导入进度失败';
|
||||||
|
document.getElementById('msgSkipPriceAsinImport').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;
|
||||||
|
setSkipPriceAsinImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
|
||||||
|
if (progress.status === 'success') {
|
||||||
|
finish();
|
||||||
|
document.getElementById('msgSkipPriceAsinImport').textContent = '导入成功:总行数 ' + totalRows + ',ASIN 数量 ' + (progress.asin_count || 0) + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
|
||||||
|
document.getElementById('msgSkipPriceAsinImport').className = 'msg ok';
|
||||||
|
loadSkipPriceAsin(1);
|
||||||
|
} else if (progress.status === 'failed') {
|
||||||
|
finish();
|
||||||
|
document.getElementById('msgSkipPriceAsinImport').textContent = progress.error_message || '导入失败';
|
||||||
|
document.getElementById('msgSkipPriceAsinImport').className = 'msg err';
|
||||||
|
} else {
|
||||||
|
schedule();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
if (!isActive()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
document.getElementById('msgSkipPriceAsinImport').textContent = '查询导入进度失败';
|
||||||
|
document.getElementById('msgSkipPriceAsinImport').className = 'msg err';
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
inFlight = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
}
|
||||||
|
function pollSkipPriceAsinDeleteImport(importId) {
|
||||||
|
stopSkipPriceAsinDeleteImportProgress();
|
||||||
|
var pollSeq = ++skipPriceAsinDeleteImportPollSeq;
|
||||||
|
var finished = false;
|
||||||
|
var inFlight = false;
|
||||||
|
var attempts = 0;
|
||||||
|
var maxAttempts = 300;
|
||||||
|
function isActive() {
|
||||||
|
return !finished && pollSeq === skipPriceAsinDeleteImportPollSeq;
|
||||||
|
}
|
||||||
|
function finish() {
|
||||||
|
finished = true;
|
||||||
|
if (skipPriceAsinDeleteImportPollTimer) {
|
||||||
|
clearTimeout(skipPriceAsinDeleteImportPollTimer);
|
||||||
|
skipPriceAsinDeleteImportPollTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function schedule() {
|
||||||
|
if (isActive()) {
|
||||||
|
skipPriceAsinDeleteImportPollTimer = setTimeout(tick, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function tick() {
|
||||||
|
if (!isActive() || inFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attempts += 1;
|
||||||
|
if (attempts > maxAttempts) {
|
||||||
|
finish();
|
||||||
|
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '查询删除进度超时,请稍后刷新列表确认结果';
|
||||||
|
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inFlight = true;
|
||||||
|
fetch('/api/admin/skip-price-asins/delete-import/' + encodeURIComponent(importId))
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (res) {
|
||||||
|
if (!isActive()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res.success) {
|
||||||
|
finish();
|
||||||
|
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = res.error || '查询删除进度失败';
|
||||||
|
document.getElementById('msgSkipPriceAsinDeleteImport').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;
|
||||||
|
setSkipPriceAsinDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
|
||||||
|
if (progress.status === 'success') {
|
||||||
|
finish();
|
||||||
|
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '删除成功:总行数 ' + totalRows + ',ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
|
||||||
|
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg ok';
|
||||||
|
loadSkipPriceAsin(1);
|
||||||
|
} else if (progress.status === 'failed') {
|
||||||
|
finish();
|
||||||
|
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = progress.error_message || '删除失败';
|
||||||
|
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err';
|
||||||
|
} else {
|
||||||
|
schedule();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
if (!isActive()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '查询删除进度失败';
|
||||||
|
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err';
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
inFlight = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
}
|
||||||
|
function uploadSkipPriceAsinImport(deleteMode) {
|
||||||
|
var fileInput = document.getElementById(deleteMode ? 'skipPriceAsinDeleteImportFile' : 'skipPriceAsinImportFile');
|
||||||
|
var msgEl = document.getElementById(deleteMode ? 'msgSkipPriceAsinDeleteImport' : 'msgSkipPriceAsinImport');
|
||||||
|
msgEl.textContent = '';
|
||||||
|
msgEl.className = 'msg';
|
||||||
|
if (deleteMode) {
|
||||||
|
stopSkipPriceAsinDeleteImportProgress();
|
||||||
|
document.getElementById('skipPriceAsinDeleteImportProgressWrap').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
stopSkipPriceAsinImportProgress();
|
||||||
|
document.getElementById('skipPriceAsinImportProgressWrap').style.display = 'none';
|
||||||
|
}
|
||||||
|
var groupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim();
|
||||||
|
if (!groupId) {
|
||||||
|
msgEl.textContent = '请先选择分组';
|
||||||
|
msgEl.className = 'msg err';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!fileInput.files || fileInput.files.length === 0) {
|
||||||
|
msgEl.textContent = '请选择 Excel 文件';
|
||||||
|
msgEl.className = 'msg err';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var file = fileInput.files[0];
|
||||||
|
var lowerName = (file.name || '').toLowerCase();
|
||||||
|
if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) {
|
||||||
|
msgEl.textContent = '仅支持 .xlsx 或 .xls 文件';
|
||||||
|
msgEl.className = 'msg err';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (deleteMode && !confirm('确定按 Excel 中的删除ASIN批量删除该店铺跳过跟价 ASIN 吗?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('group_id', groupId);
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', deleteMode ? '/api/admin/skip-price-asins/delete-import' : '/api/admin/skip-price-asins/import', true);
|
||||||
|
xhr.upload.onprogress = function (event) {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
var percent = Math.round(event.loaded * 100 / event.total);
|
||||||
|
if (deleteMode) setSkipPriceAsinDeleteImportProgress(percent, '上传中:' + percent + '%');
|
||||||
|
else setSkipPriceAsinImportProgress(percent, '上传中:' + percent + '%');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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 || (deleteMode ? '删除失败' : '导入失败');
|
||||||
|
msgEl.className = 'msg err';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (deleteMode) {
|
||||||
|
setSkipPriceAsinDeleteImportProgress(100, '上传完成,后端处理中...');
|
||||||
|
pollSkipPriceAsinDeleteImport(res.import_id);
|
||||||
|
} else {
|
||||||
|
setSkipPriceAsinImportProgress(100, '上传完成,后端处理中...');
|
||||||
|
pollSkipPriceAsinImport(res.import_id);
|
||||||
|
}
|
||||||
|
fileInput.value = '';
|
||||||
|
};
|
||||||
|
xhr.onerror = function () {
|
||||||
|
msgEl.textContent = '请求失败';
|
||||||
|
msgEl.className = 'msg err';
|
||||||
|
};
|
||||||
|
xhr.send(formData);
|
||||||
|
}
|
||||||
document.getElementById('btnChooseSkipPriceAsinShop').onclick = openChooseSkipPriceAsinShopModal;
|
document.getElementById('btnChooseSkipPriceAsinShop').onclick = openChooseSkipPriceAsinShopModal;
|
||||||
document.getElementById('btnSearchChooseSkipPriceAsinShop').onclick = function () {
|
document.getElementById('btnSearchChooseSkipPriceAsinShop').onclick = function () {
|
||||||
loadChooseSkipPriceAsinShops(1);
|
loadChooseSkipPriceAsinShops(1);
|
||||||
@@ -3590,6 +4048,12 @@
|
|||||||
};
|
};
|
||||||
setupSkipPriceAsinShopPicker();
|
setupSkipPriceAsinShopPicker();
|
||||||
initDropdownMultiSelect('skipPriceAsinCountries', '请选择国家');
|
initDropdownMultiSelect('skipPriceAsinCountries', '请选择国家');
|
||||||
|
document.getElementById('btnImportSkipPriceAsin').onclick = function () {
|
||||||
|
uploadSkipPriceAsinImport(false);
|
||||||
|
};
|
||||||
|
document.getElementById('btnDeleteImportSkipPriceAsin').onclick = function () {
|
||||||
|
uploadSkipPriceAsinImport(true);
|
||||||
|
};
|
||||||
renderSkipPriceAsinInputs();
|
renderSkipPriceAsinInputs();
|
||||||
|
|
||||||
// ========== 查询 ASIN ==========
|
// ========== 查询 ASIN ==========
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ const parsing = ref(false)
|
|||||||
const pushing = ref(false)
|
const pushing = ref(false)
|
||||||
const queuePayloadText = ref('')
|
const queuePayloadText = ref('')
|
||||||
const pollingTaskIds = ref<number[]>([])
|
const pollingTaskIds = ref<number[]>([])
|
||||||
|
const pendingFileTaskIds = ref<number[]>([])
|
||||||
const pollTimer = ref<number | null>(null)
|
const pollTimer = ref<number | null>(null)
|
||||||
const pollingInFlight = ref(false)
|
const pollingInFlight = ref(false)
|
||||||
let disposed = false
|
let disposed = false
|
||||||
@@ -204,12 +205,14 @@ function pollingKey() {
|
|||||||
|
|
||||||
function savePollingIds() {
|
function savePollingIds() {
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
if (!pollingTaskIds.value.length) window.localStorage.removeItem(pollingKey())
|
const ids = Array.from(new Set([...pollingTaskIds.value, ...pendingFileTaskIds.value]))
|
||||||
else window.localStorage.setItem(pollingKey(), JSON.stringify(pollingTaskIds.value))
|
if (!ids.length) window.localStorage.removeItem(pollingKey())
|
||||||
|
else window.localStorage.setItem(pollingKey(), JSON.stringify(ids))
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearPollingIds() {
|
function clearPollingIds() {
|
||||||
pollingTaskIds.value = []
|
pollingTaskIds.value = []
|
||||||
|
pendingFileTaskIds.value = []
|
||||||
savePollingIds()
|
savePollingIds()
|
||||||
stopPolling()
|
stopPolling()
|
||||||
}
|
}
|
||||||
@@ -391,6 +394,18 @@ function removePollingTask(taskId: number) {
|
|||||||
savePollingIds()
|
savePollingIds()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addPendingFileTask(taskId: number) {
|
||||||
|
if (!pendingFileTaskIds.value.includes(taskId)) {
|
||||||
|
pendingFileTaskIds.value = [...pendingFileTaskIds.value, taskId]
|
||||||
|
savePollingIds()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePendingFileTask(taskId: number) {
|
||||||
|
pendingFileTaskIds.value = pendingFileTaskIds.value.filter((id) => id !== taskId)
|
||||||
|
savePollingIds()
|
||||||
|
}
|
||||||
|
|
||||||
function ensurePolling() {
|
function ensurePolling() {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
if (pollTimer.value != null) return
|
if (pollTimer.value != null) return
|
||||||
@@ -414,9 +429,9 @@ function scheduleNextPoll(immediate = false) {
|
|||||||
const run = async () => {
|
const run = async () => {
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
if (!pollingTaskIds.value.length) return
|
if (!pollingTaskIds.value.length && !pendingFileTaskIds.value.length) return
|
||||||
await refreshTaskProgress()
|
await refreshTaskProgress()
|
||||||
if (!disposed && pollingTaskIds.value.length) {
|
if (!disposed && (pollingTaskIds.value.length || pendingFileTaskIds.value.length)) {
|
||||||
pollTimer.value = timers.setTimeout('task-poll', run, getTaskPollIntervalMs())
|
pollTimer.value = timers.setTimeout('task-poll', run, getTaskPollIntervalMs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,34 +440,68 @@ function scheduleNextPoll(immediate = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshTaskProgress() {
|
async function refreshTaskProgress() {
|
||||||
if (pollingInFlight.value || !pollingTaskIds.value.length) {
|
if (pollingInFlight.value || (!pollingTaskIds.value.length && !pendingFileTaskIds.value.length)) {
|
||||||
if (!pollingTaskIds.value.length) stopPolling()
|
if (!pollingTaskIds.value.length && !pendingFileTaskIds.value.length) stopPolling()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pollingInFlight.value = true
|
pollingInFlight.value = true
|
||||||
try {
|
try {
|
||||||
const batch = await getAppearancePatentTaskProgressBatch(pollingTaskIds.value)
|
let shouldRefreshHistory = pendingFileTaskIds.value.length > 0
|
||||||
let hasTerminalTask = false
|
if (pollingTaskIds.value.length) {
|
||||||
for (const detail of batch.items || []) {
|
const batch = await getAppearancePatentTaskProgressBatch(pollingTaskIds.value)
|
||||||
const task = detail.task
|
for (const detail of batch.items || []) {
|
||||||
if (!task?.id) continue
|
const task = detail.task
|
||||||
if (task.status === 'SUCCESS' || task.status === 'FAILED') {
|
if (!task?.id) continue
|
||||||
removePollingTask(task.id)
|
if (task.status === 'SUCCESS' || task.status === 'FAILED') {
|
||||||
hasTerminalTask = true
|
removePollingTask(task.id)
|
||||||
|
shouldRefreshHistory = true
|
||||||
|
if (task.status === 'SUCCESS') addPendingFileTask(task.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (batch.missingTaskIds?.length) {
|
||||||
|
batch.missingTaskIds.forEach((taskId) => {
|
||||||
|
removePollingTask(taskId)
|
||||||
|
removePendingFileTask(taskId)
|
||||||
|
})
|
||||||
|
shouldRefreshHistory = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (batch.missingTaskIds?.length) {
|
if (shouldRefreshHistory) {
|
||||||
batch.missingTaskIds.forEach((taskId) => removePollingTask(taskId))
|
|
||||||
}
|
|
||||||
if (hasTerminalTask) {
|
|
||||||
await loadDashboard()
|
await loadDashboard()
|
||||||
await loadHistory()
|
await loadHistory()
|
||||||
|
settlePendingFileTasks()
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
pollingInFlight.value = false
|
pollingInFlight.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function settlePendingFileTasks() {
|
||||||
|
if (!pendingFileTaskIds.value.length) return
|
||||||
|
const remaining: number[] = []
|
||||||
|
for (const taskId of pendingFileTaskIds.value) {
|
||||||
|
const item = historyItems.value.find((row) => row.taskId === taskId)
|
||||||
|
if (!item) continue
|
||||||
|
const taskStatus = (item.taskStatus || '').toUpperCase()
|
||||||
|
const fileStatus = (item.fileStatus || '').toUpperCase()
|
||||||
|
if (taskStatus === 'FAILED' || item.fileReady || fileStatus === 'SUCCESS' || fileStatus === 'FAILED') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remaining.push(taskId)
|
||||||
|
}
|
||||||
|
pendingFileTaskIds.value = remaining
|
||||||
|
savePollingIds()
|
||||||
|
}
|
||||||
|
|
||||||
|
function seedPendingFileTasksFromHistory() {
|
||||||
|
const ids = historyItems.value
|
||||||
|
.filter((item) => item.taskId != null && isResultPreparing(item))
|
||||||
|
.map((item) => item.taskId as number)
|
||||||
|
pendingFileTaskIds.value = Array.from(new Set(ids))
|
||||||
|
savePollingIds()
|
||||||
|
if (pendingFileTaskIds.value.length) ensurePolling()
|
||||||
|
}
|
||||||
|
|
||||||
async function loadDashboard() {
|
async function loadDashboard() {
|
||||||
dashboard.value = await getAppearancePatentDashboard()
|
dashboard.value = await getAppearancePatentDashboard()
|
||||||
}
|
}
|
||||||
@@ -551,6 +600,7 @@ onMounted(async () => {
|
|||||||
loadDashboard().catch(() => undefined),
|
loadDashboard().catch(() => undefined),
|
||||||
loadHistory().catch(() => undefined),
|
loadHistory().catch(() => undefined),
|
||||||
])
|
])
|
||||||
|
seedPendingFileTasksFromHistory()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@@ -940,9 +940,6 @@ async function runMatch() {
|
|||||||
const batch = res.items || []
|
const batch = res.items || []
|
||||||
matchedItems.value = mergeMatchedItems(matchedItems.value, batch)
|
matchedItems.value = mergeMatchedItems(matchedItems.value, batch)
|
||||||
saveMatchedItemsToStorage()
|
saveMatchedItemsToStorage()
|
||||||
if (autoQueueEnabled.value) {
|
|
||||||
void processMatchedQueue()
|
|
||||||
}
|
|
||||||
if (!batch.length) {
|
if (!batch.length) {
|
||||||
ElMessage.warning('未返回匹配结果')
|
ElMessage.warning('未返回匹配结果')
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -521,6 +521,7 @@ export function runDeleteBrand(
|
|||||||
...request,
|
...request,
|
||||||
user_id: getCurrentUserId(),
|
user_id: getCurrentUserId(),
|
||||||
},
|
},
|
||||||
|
{ timeout: 180000 },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user