更新带货视频工作流接口和页面
This commit is contained in:
+29
-9
@@ -1,35 +1,55 @@
|
||||
package com.nanri.aiimage.modules.pricetrack.model.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 跟价跳过 ASIN 分页返回
|
||||
* Price-track skipped ASIN page response.
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "跟价跳过 ASIN 分页返回")
|
||||
@Schema(description = "Price-track skipped ASIN page response")
|
||||
public class SkipPriceAsinPageVo {
|
||||
|
||||
@Schema(description = "当前页码,从 1 开始")
|
||||
@Schema(description = "Current page, starting from 1")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页条数")
|
||||
@Schema(description = "Page size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "总记录数")
|
||||
@Schema(description = "Total records")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "总页数")
|
||||
@Schema(description = "Total pages")
|
||||
private Integer totalPages;
|
||||
|
||||
@Schema(description = "按国家分组的 ASIN 列表(简化版,仅 ASIN)")
|
||||
@Schema(description = "ASIN list grouped by country")
|
||||
private Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
|
||||
|
||||
@Schema(description = "按国家分组的 ASIN 详情列表(包含 asin 和 minimumPrice)")
|
||||
@Schema(description = "ASIN detail list grouped by country, including asin and minimumPrice")
|
||||
private Map<String, List<Map<String, String>>> skipAsinDetailsByCountry = new LinkedHashMap<>();
|
||||
|
||||
@JsonProperty("skip_asins")
|
||||
@Schema(description = "Legacy Python queue field: ASIN list grouped by country")
|
||||
private Map<String, List<String>> skipAsins = new LinkedHashMap<>();
|
||||
|
||||
@JsonProperty("skip_asins_by_country")
|
||||
@Schema(description = "Legacy Python queue field: ASIN list grouped by country")
|
||||
private Map<String, List<String>> skipAsinsByCountryLegacy = new LinkedHashMap<>();
|
||||
|
||||
@JsonProperty("skip_asin_details_by_country")
|
||||
@Schema(description = "Legacy Python queue field: ASIN detail list grouped by country")
|
||||
private Map<String, List<Map<String, String>>> skipAsinDetailsByCountryLegacy = new LinkedHashMap<>();
|
||||
|
||||
@JsonProperty("asin_rows_by_country")
|
||||
@Schema(description = "Legacy Python queue field: appointed ASIN rows grouped by country")
|
||||
private Map<String, List<Map<String, String>>> asinRowsByCountry = new LinkedHashMap<>();
|
||||
|
||||
@JsonProperty("minimum_price_by_country_and_asin")
|
||||
@Schema(description = "Legacy Python queue field: minimum price map grouped by country and ASIN")
|
||||
private Map<String, Map<String, String>> minimumPriceByCountryAndAsin = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
+261
-11
@@ -61,6 +61,7 @@ public class PriceTrackTaskService {
|
||||
|
||||
private static final String MODULE_TYPE = "PRICE_TRACK";
|
||||
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
private static final String ASIN_ROWS_PAYLOAD_SCOPE = "price-track-asin-rows";
|
||||
|
||||
private final FileTaskMapper fileTaskMapper;
|
||||
private final FileResultMapper fileResultMapper;
|
||||
@@ -333,7 +334,7 @@ public class PriceTrackTaskService {
|
||||
if (taskIds == null || taskIds.isEmpty()) {
|
||||
return batch;
|
||||
}
|
||||
Map<Long, FileTaskEntity> taskMap = loadTaskMapByIds(taskIds);
|
||||
Map<Long, FileTaskEntity> taskMap = loadTaskProgressMapByIds(taskIds);
|
||||
for (Long taskId : taskIds) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
continue;
|
||||
@@ -350,6 +351,41 @@ public class PriceTrackTaskService {
|
||||
return batch;
|
||||
}
|
||||
|
||||
private Map<Long, FileTaskEntity> loadTaskProgressMapByIds(List<Long> taskIds) {
|
||||
Map<Long, FileTaskEntity> result = new LinkedHashMap<>();
|
||||
if (taskIds == null || taskIds.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<Long> normalizedTaskIds = taskIds.stream()
|
||||
.filter(taskId -> taskId != null && taskId > 0)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (normalizedTaskIds.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
int batchSize = Math.max(1, taskPressureProperties.getDbSelectBatchSize());
|
||||
for (int start = 0; start < normalizedTaskIds.size(); start += batchSize) {
|
||||
int end = Math.min(start + batchSize, normalizedTaskIds.size());
|
||||
List<FileTaskEntity> tasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||
.select(FileTaskEntity::getId,
|
||||
FileTaskEntity::getTaskNo,
|
||||
FileTaskEntity::getStatus,
|
||||
FileTaskEntity::getErrorMessage,
|
||||
FileTaskEntity::getCreatedAt,
|
||||
FileTaskEntity::getUpdatedAt,
|
||||
FileTaskEntity::getFinishedAt,
|
||||
FileTaskEntity::getRequestJson)
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
|
||||
.in(FileTaskEntity::getId, normalizedTaskIds.subList(start, end)));
|
||||
for (FileTaskEntity task : tasks) {
|
||||
if (task != null && task.getId() != null) {
|
||||
result.put(task.getId(), task);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PriceTrackCreateTaskVo createTask(PriceTrackCreateTaskRequest request) {
|
||||
if (request.isStatusMode() == request.isAsinMode()) throw new BusinessException("跟价模式必须二选一");
|
||||
@@ -372,7 +408,11 @@ public class PriceTrackTaskService {
|
||||
Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
|
||||
Map<String, List<Map<String, String>>> skipAsinDetailsByCountry = new LinkedHashMap<>();
|
||||
Map<String, List<Map<String, String>>> asinRowsByCountry = new LinkedHashMap<>();
|
||||
Map<String, List<Map<String, String>>> asinRowsForPayload = new LinkedHashMap<>();
|
||||
Map<String, Map<String, String>> minimumPriceByCountryAndAsin = new LinkedHashMap<>();
|
||||
if (request.isAsinMode()) {
|
||||
asinRowsForPayload = parseAsinRowsByCountry(request.getAsinFiles(), request.getCountryCodes());
|
||||
}
|
||||
log.info("[price-track] createTask skipAsins countries={} asinMode={}",
|
||||
skipAsinsByCountry.keySet(), request.isAsinMode());
|
||||
|
||||
@@ -391,6 +431,9 @@ public class PriceTrackTaskService {
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
fileTaskMapper.insert(task);
|
||||
priceTrackTaskCacheService.touchTaskHeartbeat(task.getId());
|
||||
if (request.isAsinMode()) {
|
||||
saveTaskAsinRowsPayload(task.getId(), asinRowsForPayload);
|
||||
}
|
||||
if (request.getLoopRunId() != null) {
|
||||
priceTrackLoopRunService.bindChildTask(request.getLoopRunId(), task.getId(), request.getRoundIndex(), request.getShopIndex());
|
||||
}
|
||||
@@ -507,7 +550,7 @@ public class PriceTrackTaskService {
|
||||
continue;
|
||||
}
|
||||
if (countPayloadRows(merged) <= 0) {
|
||||
markResultFailed(fr, "no usable price-track rows received");
|
||||
markResultFailed(fr, buildNoUsableRowsMessage(payload));
|
||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||
continue;
|
||||
}
|
||||
@@ -584,7 +627,7 @@ public class PriceTrackTaskService {
|
||||
continue;
|
||||
}
|
||||
if (countPayloadRows(cachedPayload) <= 0) {
|
||||
markResultFailed(fr, "no usable price-track rows received before interruption");
|
||||
markResultFailed(fr, buildNoUsableRowsMessage(cachedPayload));
|
||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||
changed = true;
|
||||
log.warn("[price-track] stale finalize finished without data taskId={} shop={} fromCompensation={}",
|
||||
@@ -784,21 +827,42 @@ public class PriceTrackTaskService {
|
||||
|
||||
PriceTrackCreateTaskRequest request = parseTaskRequest(task);
|
||||
|
||||
// 从任务中获取国家代码
|
||||
// Resolve country codes from task request.
|
||||
List<String> countryCodes = request.getCountryCodes();
|
||||
|
||||
// 判断是全量模式还是文件模式
|
||||
// Choose full mode or uploaded-ASIN file mode.
|
||||
boolean isAsinMode = request.isAsinMode();
|
||||
|
||||
if (isAsinMode) {
|
||||
// 文件模式:从任务 requestJson 中的上传文件解析数据并分页返回
|
||||
Map<String, List<Map<String, String>>> storedRows = loadTaskAsinRowsPayload(taskId);
|
||||
if (storedRows != null) {
|
||||
return getTaskAsinRowsPaginated(storedRows, countryCodes, page, pageSize);
|
||||
}
|
||||
// File mode reads parsed uploaded ASIN rows.
|
||||
return getTaskAsinFilePaginated(request, countryCodes, page, pageSize);
|
||||
} else {
|
||||
// 全量模式:从 biz_skip_price_asin 表查询
|
||||
return skipPriceAsinService.listSkipAsinsPaginated(null, countryCodes, page, pageSize);
|
||||
// Full mode uses all ASINs and minimum prices from biz_skip_price_asin.
|
||||
return skipPriceAsinService.listSkipAsinsPaginated(List.of(), countryCodes, page, pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> resolveTaskShopNames(PriceTrackCreateTaskRequest request) {
|
||||
List<String> shopNames = new ArrayList<>();
|
||||
if (request == null || request.getItems() == null) {
|
||||
return shopNames;
|
||||
}
|
||||
for (PriceTrackMatchShopsVo.PriceTrackShopQueueItem item : request.getItems()) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
String normalized = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
|
||||
if (!normalized.isBlank() && !shopNames.contains(normalized)) {
|
||||
shopNames.add(normalized);
|
||||
}
|
||||
}
|
||||
return shopNames;
|
||||
}
|
||||
|
||||
public SkipPriceAsinCheckVo checkTaskSkipAsin(Long taskId, String country, String asin) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
throw new BusinessException("taskId 不合法");
|
||||
@@ -909,11 +973,13 @@ public class PriceTrackTaskService {
|
||||
// 提取当前页的数据(跨国家顺序提取)
|
||||
Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
|
||||
Map<String, List<Map<String, String>>> skipDetailsByCountry = new LinkedHashMap<>();
|
||||
Map<String, List<Map<String, String>>> asinRowsByCountry = new LinkedHashMap<>();
|
||||
|
||||
// 初始化所有目标国家
|
||||
for (String country : targetCountries) {
|
||||
skipAsinsByCountry.put(country, new ArrayList<>());
|
||||
skipDetailsByCountry.put(country, new ArrayList<>());
|
||||
asinRowsByCountry.put(country, new ArrayList<>());
|
||||
}
|
||||
|
||||
int currentIndex = 0;
|
||||
@@ -938,6 +1004,7 @@ public class PriceTrackTaskService {
|
||||
detail.put("asin", asin);
|
||||
detail.put("minimumPrice", minimumPrice == null ? "" : minimumPrice);
|
||||
skipDetailsByCountry.get(country).add(detail);
|
||||
asinRowsByCountry.get(country).add(new LinkedHashMap<>(row));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -960,10 +1027,168 @@ public class PriceTrackTaskService {
|
||||
vo.setTotalPages(totalPages);
|
||||
vo.setSkipAsinsByCountry(skipAsinsByCountry);
|
||||
vo.setSkipAsinDetailsByCountry(skipDetailsByCountry);
|
||||
vo.setSkipAsins(skipAsinsByCountry);
|
||||
vo.setSkipAsinsByCountryLegacy(skipAsinsByCountry);
|
||||
vo.setSkipAsinDetailsByCountryLegacy(skipDetailsByCountry);
|
||||
vo.setAsinRowsByCountry(asinRowsByCountry);
|
||||
vo.setMinimumPriceByCountryAndAsin(buildMinimumPriceByCountryAndAsin(asinRowsByCountry));
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
private com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo getTaskAsinRowsPaginated(
|
||||
Map<String, List<Map<String, String>>> allAsinRows, List<String> countryCodes, int page, int pageSize) {
|
||||
if (page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
if (pageSize < 1 || pageSize > 2000) {
|
||||
pageSize = 1000;
|
||||
}
|
||||
if (allAsinRows == null) {
|
||||
allAsinRows = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
List<String> targetCountries = (countryCodes == null || countryCodes.isEmpty())
|
||||
? new ArrayList<>(allAsinRows.keySet())
|
||||
: countryCodes;
|
||||
|
||||
long totalRows = 0;
|
||||
for (String country : targetCountries) {
|
||||
List<Map<String, String>> rows = allAsinRows.getOrDefault(country, List.of());
|
||||
totalRows += rows.size();
|
||||
}
|
||||
|
||||
int totalPages = (int) ((totalRows + pageSize - 1) / pageSize);
|
||||
int startIndex = (page - 1) * pageSize;
|
||||
int endIndex = Math.min(startIndex + pageSize, (int) totalRows);
|
||||
|
||||
Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
|
||||
Map<String, List<Map<String, String>>> skipDetailsByCountry = new LinkedHashMap<>();
|
||||
Map<String, List<Map<String, String>>> asinRowsByCountry = new LinkedHashMap<>();
|
||||
for (String country : targetCountries) {
|
||||
skipAsinsByCountry.put(country, new ArrayList<>());
|
||||
skipDetailsByCountry.put(country, new ArrayList<>());
|
||||
asinRowsByCountry.put(country, new ArrayList<>());
|
||||
}
|
||||
|
||||
int currentIndex = 0;
|
||||
boolean shouldBreak = false;
|
||||
for (String country : targetCountries) {
|
||||
if (shouldBreak) {
|
||||
break;
|
||||
}
|
||||
List<Map<String, String>> countryRows = allAsinRows.getOrDefault(country, List.of());
|
||||
for (Map<String, String> row : countryRows) {
|
||||
if (currentIndex >= startIndex && currentIndex < endIndex) {
|
||||
String asin = row.get("asin");
|
||||
String minimumPrice = row.get("minimumPrice");
|
||||
if (asin != null && !asin.isBlank()) {
|
||||
skipAsinsByCountry.get(country).add(asin);
|
||||
Map<String, String> detail = new LinkedHashMap<>();
|
||||
detail.put("asin", asin);
|
||||
detail.put("minimumPrice", minimumPrice == null ? "" : minimumPrice);
|
||||
skipDetailsByCountry.get(country).add(detail);
|
||||
asinRowsByCountry.get(country).add(new LinkedHashMap<>(row));
|
||||
}
|
||||
}
|
||||
currentIndex++;
|
||||
if (currentIndex >= endIndex) {
|
||||
shouldBreak = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo vo =
|
||||
new com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo();
|
||||
vo.setPage(page);
|
||||
vo.setPageSize(pageSize);
|
||||
vo.setTotal(totalRows);
|
||||
vo.setTotalPages(totalPages);
|
||||
vo.setSkipAsinsByCountry(skipAsinsByCountry);
|
||||
vo.setSkipAsinDetailsByCountry(skipDetailsByCountry);
|
||||
vo.setSkipAsins(skipAsinsByCountry);
|
||||
vo.setSkipAsinsByCountryLegacy(skipAsinsByCountry);
|
||||
vo.setSkipAsinDetailsByCountryLegacy(skipDetailsByCountry);
|
||||
vo.setAsinRowsByCountry(asinRowsByCountry);
|
||||
vo.setMinimumPriceByCountryAndAsin(buildMinimumPriceByCountryAndAsin(asinRowsByCountry));
|
||||
return vo;
|
||||
}
|
||||
|
||||
private void saveTaskAsinRowsPayload(Long taskId, Map<String, List<Map<String, String>>> asinRowsByCountry) {
|
||||
if (asinRowsByCountry == null) {
|
||||
return;
|
||||
}
|
||||
taskResultPayloadService.saveLatest(taskId, MODULE_TYPE, ASIN_ROWS_PAYLOAD_SCOPE, asinRowsByCountry);
|
||||
log.info("[price-track] saved asin rows payload taskId={} countries={} rows={}",
|
||||
taskId, asinRowsByCountry.keySet(), countAsinRows(asinRowsByCountry));
|
||||
}
|
||||
|
||||
private Map<String, List<Map<String, String>>> loadTaskAsinRowsPayload(Long taskId) {
|
||||
try {
|
||||
Object payload = taskResultPayloadService.getLatest(taskId, MODULE_TYPE, ASIN_ROWS_PAYLOAD_SCOPE, Map.class);
|
||||
if (payload == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, List<Map<String, String>>> rows = normalizeAsinRowsPayload(payload);
|
||||
if (!rows.isEmpty()) {
|
||||
log.info("[price-track] loaded asin rows payload taskId={} countries={} rows={}",
|
||||
taskId, rows.keySet(), countAsinRows(rows));
|
||||
}
|
||||
return rows;
|
||||
} catch (Exception ex) {
|
||||
log.warn("[price-track] load asin rows payload failed taskId={} err={}", taskId, ex.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, List<Map<String, String>>> normalizeAsinRowsPayload(Object payload) {
|
||||
Map<String, List<Map<String, String>>> out = new LinkedHashMap<>();
|
||||
if (!(payload instanceof Map<?, ?> rawMap)) {
|
||||
return out;
|
||||
}
|
||||
for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
|
||||
String country = Objects.toString(entry.getKey(), "").trim().toUpperCase(Locale.ROOT);
|
||||
if (country.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
List<Map<String, String>> rows = new ArrayList<>();
|
||||
if (entry.getValue() instanceof List<?> rawRows) {
|
||||
for (Object rawRow : rawRows) {
|
||||
if (!(rawRow instanceof Map<?, ?> rawRowMap)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, String> row = new LinkedHashMap<>();
|
||||
for (Map.Entry<?, ?> cell : rawRowMap.entrySet()) {
|
||||
String key = Objects.toString(cell.getKey(), "").trim();
|
||||
if (key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
row.put(key, Objects.toString(cell.getValue(), ""));
|
||||
}
|
||||
if (!row.isEmpty()) {
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.put(country, rows);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private long countAsinRows(Map<String, List<Map<String, String>>> asinRowsByCountry) {
|
||||
if (asinRowsByCountry == null || asinRowsByCountry.isEmpty()) {
|
||||
return 0L;
|
||||
}
|
||||
long total = 0L;
|
||||
for (List<Map<String, String>> rows : asinRowsByCountry.values()) {
|
||||
if (rows != null) {
|
||||
total += rows.size();
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private Map<String, List<Map<String, String>>> parseAsinRowsByCountry(List<String> asinFiles, List<String> countryCodes) {
|
||||
Map<String, List<Map<String, String>>> merged = new LinkedHashMap<>();
|
||||
if (asinFiles == null || asinFiles.isEmpty()) {
|
||||
@@ -1149,7 +1374,7 @@ public class PriceTrackTaskService {
|
||||
}
|
||||
|
||||
private boolean isEmptyAsinRow(Map<String, String> row) {
|
||||
return row.values().stream().allMatch(value -> value == null || value.isBlank());
|
||||
return row == null || normalizeCellText(row.get("asin")).isBlank();
|
||||
}
|
||||
|
||||
private String mapHeaderKey(String rawHeader) {
|
||||
@@ -1159,7 +1384,9 @@ public class PriceTrackTaskService {
|
||||
}
|
||||
return switch (header) {
|
||||
case "店铺商城名称", "shopmallname" -> "shopMallName";
|
||||
case "asin" -> "asin";
|
||||
case "asin", "asin码", "asin编码", "商品asin", "商品编码", "商品编号",
|
||||
"子asin", "父asin", "amazonasin", "amazon_asin", "amazon asin",
|
||||
"sellerasin", "seller_asin", "seller asin" -> "asin";
|
||||
case "价格", "price" -> "price";
|
||||
case "推荐价", "recommendedprice" -> "recommendedPrice";
|
||||
case "最低价", "minimumprice" -> "minimumPrice";
|
||||
@@ -1498,7 +1725,30 @@ public class PriceTrackTaskService {
|
||||
if (payload == null) {
|
||||
return 0;
|
||||
}
|
||||
return excelAssemblyService.countRows(excelAssemblyService.normalizeCountriesMap(payload.getCountries()));
|
||||
Map<String, List<PriceTrackSubmitResultRequest.AsinResult>> countries =
|
||||
excelAssemblyService.normalizeCountriesMap(payload.getCountries());
|
||||
if (countries.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
for (List<PriceTrackSubmitResultRequest.AsinResult> rows : countries.values()) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (PriceTrackSubmitResultRequest.AsinResult row : rows) {
|
||||
if (row != null && hasText(row.getAsin())) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private String buildNoUsableRowsMessage(PriceTrackSubmitResultRequest.ShopResult payload) {
|
||||
if (payload != null && hasText(payload.getError())) {
|
||||
return payload.getError().trim();
|
||||
}
|
||||
return "no usable price-track rows received";
|
||||
}
|
||||
private String firstNonBlank(String preferred, String fallback) {
|
||||
if (preferred != null && !preferred.isBlank()) {
|
||||
|
||||
Reference in New Issue
Block a user