Merge remote-tracking branch 'origin/test' into dev
This commit is contained in:
+46
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.order.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.core.export.ExportContracts.ExportBatchRequest;
|
||||
import com.baiyue.common.core.export.ExportContracts.ExportBatchResponse;
|
||||
import com.baiyue.order.service.DomesticWarehouseOrderExportProviderService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
/** 仅供 File 异步导出中心调用的国内仓订单数据 Provider。 */
|
||||
@RestController
|
||||
@RequestMapping("/internal/exports/v1/DOMESTIC_WAREHOUSE_ORDER_LIST")
|
||||
public class DomesticWarehouseOrderExportProviderController {
|
||||
@Resource private DomesticWarehouseOrderExportProviderService service;
|
||||
@Value("${baiyue.security.internal-service-token}") private String configuredToken;
|
||||
private byte[] internalToken;
|
||||
|
||||
@PostConstruct
|
||||
void initializeToken() {
|
||||
byte[] value = configuredToken == null ? new byte[0] : configuredToken.getBytes(StandardCharsets.UTF_8);
|
||||
if (value.length < 32) throw new IllegalStateException("INTERNAL_SERVICE_TOKEN 至少需要 32 字节");
|
||||
internalToken = value;
|
||||
}
|
||||
|
||||
@PostMapping("/batches")
|
||||
public ApiResponse<ExportBatchResponse> batch(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@RequestBody ExportBatchRequest request) {
|
||||
byte[] supplied = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8);
|
||||
if (!MessageDigest.isEqual(internalToken, supplied)) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "Invalid internal service token");
|
||||
}
|
||||
return ApiResponse.success(service.exportBatch(request));
|
||||
}
|
||||
}
|
||||
@@ -261,6 +261,13 @@ public class OrderRepository {
|
||||
.in(OrderEntity::getId, orderIds));
|
||||
}
|
||||
|
||||
public List<OrderEntity> findByOrderNos(List<String> orderNos) {
|
||||
if (orderNos == null || orderNos.isEmpty()) return List.of();
|
||||
return orderMapper.selectList(new LambdaQueryWrapper<OrderEntity>()
|
||||
.in(OrderEntity::getOrderNo, orderNos)
|
||||
.eq(OrderEntity::getDeleted, false));
|
||||
}
|
||||
|
||||
public List<OrderEntity> findChildrenByParentOrders(List<OrderEntity> parentOrders) {
|
||||
if (parentOrders.isEmpty()) return List.of();
|
||||
List<Long> tenantIds = parentOrders.stream().map(OrderEntity::getTenantId).distinct().toList();
|
||||
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.core.export.ExportContracts.ExportBatchRequest;
|
||||
import com.baiyue.common.core.export.ExportContracts.ExportBatchResponse;
|
||||
import com.baiyue.common.core.export.ExportContracts.ExportColumn;
|
||||
import com.baiyue.common.core.export.ExportContracts.ExportColumnType;
|
||||
import com.baiyue.order.dto.UnifiedOrderQuery;
|
||||
import com.baiyue.order.dto.UnifiedOrderResponse;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** 为 File 提供分页数据,不在 Order 内生成文件。 */
|
||||
@Service
|
||||
public class DomesticWarehouseOrderExportProviderService {
|
||||
static final String EXPORT_TYPE = "DOMESTIC_WAREHOUSE_ORDER_LIST";
|
||||
static final int SCHEMA_VERSION = 1;
|
||||
static final int MAX_SELECTION = 5_000;
|
||||
static final long MAX_ROWS = 200_000;
|
||||
private static final int MAX_BATCH_SIZE = 500;
|
||||
private static final Set<String> PARAMETER_KEYS = Set.of(
|
||||
"selectedOrderNos", "tenantId", "keyword", "orderNo", "mainStatus", "customerId",
|
||||
"receivingWarehouseId", "destinationWarehouseId", "inboundCode", "internalOrderStatus",
|
||||
"expectedArrivalDate", "feeReviewStatus", "channelName", "containerNo", "cabinetNo",
|
||||
"sailingDate", "loaded", "fullContainer", "measurementReviewStatus", "fbaCode",
|
||||
"createdFrom", "createdTo");
|
||||
private static final List<ExportColumn> COLUMNS = List.of(
|
||||
column("inboundCode", "入仓码", ExportColumnType.STRING, 24),
|
||||
column("orderNo", "订单号", ExportColumnType.STRING, 24),
|
||||
column("mainStatus", "订单主状态", ExportColumnType.STRING, 16),
|
||||
column("customerName", "客户", ExportColumnType.STRING, 24),
|
||||
column("channelName", "渠道", ExportColumnType.STRING, 20),
|
||||
column("receivingWarehouseName", "定位仓库", ExportColumnType.STRING, 20),
|
||||
column("destinationWarehouseName", "交货仓", ExportColumnType.STRING, 20),
|
||||
column("expectedShipDate", "预计出运日期", ExportColumnType.DATE, 14),
|
||||
column("totalPackageCount", "箱数", ExportColumnType.INTEGER, 12),
|
||||
column("totalQuantity", "产品数量", ExportColumnType.INTEGER, 12),
|
||||
column("actualGrossWeightKg", "实测毛重(kg)", ExportColumnType.DECIMAL, 16),
|
||||
column("actualVolumeCbm", "实测体积(m³)", ExportColumnType.DECIMAL, 16),
|
||||
column("chargeableWeightKg", "计费重量(kg)", ExportColumnType.DECIMAL, 16),
|
||||
column("chargeableVolumeCbm", "计费体积(m³)", ExportColumnType.DECIMAL, 16),
|
||||
column("feeReviewStatus", "费用审核状态", ExportColumnType.STRING, 16),
|
||||
column("skuNos", "SKU", ExportColumnType.STRING, 28),
|
||||
column("taxMode", "报关方式", ExportColumnType.STRING, 16),
|
||||
column("containerMode", "装柜方式", ExportColumnType.STRING, 14),
|
||||
column("customsInspection", "是否商检", ExportColumnType.BOOLEAN, 12),
|
||||
column("containerNo", "集装箱号", ExportColumnType.STRING, 20),
|
||||
column("cabinetNo", "柜次号", ExportColumnType.STRING, 20),
|
||||
column("vesselName", "船名", ExportColumnType.STRING, 18),
|
||||
column("voyageNo", "航次", ExportColumnType.STRING, 16),
|
||||
column("createdAt", "创建时间", ExportColumnType.DATETIME, 21));
|
||||
private static final String SCHEMA_HASH = schemaHash(COLUMNS);
|
||||
|
||||
@Resource private UnifiedOrderQueryService orders;
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalServiceToken;
|
||||
private byte[] signingKey;
|
||||
|
||||
@PostConstruct
|
||||
void initializeSigningKey() { signingKey = internalServiceToken.getBytes(StandardCharsets.UTF_8); }
|
||||
|
||||
public ExportBatchResponse exportBatch(ExportBatchRequest request) {
|
||||
Validated validated = validate(request);
|
||||
String snapshot = validated.snapshotToken() == null ? sign(validated) : verify(validated);
|
||||
return validated.selectedOrderNos().isEmpty()
|
||||
? filtered(validated, snapshot) : selected(validated, snapshot);
|
||||
}
|
||||
|
||||
private ExportBatchResponse selected(Validated value, String snapshot) {
|
||||
int offset = cursor(value.cursor(), 0);
|
||||
if (offset > value.selectedOrderNos().size()) throw invalid("cursor exceeds selectedOrderNos");
|
||||
if (offset == 0) {
|
||||
List<UnifiedOrderResponse.Row> visible = orders.platformRowsByOrderNos(value.selectedOrderNos());
|
||||
if (visible.size() != value.selectedOrderNos().size()) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "所选订单不存在或不可见");
|
||||
}
|
||||
}
|
||||
int end = Math.min(offset + value.limit(), value.selectedOrderNos().size());
|
||||
List<String> batchOrderNos = value.selectedOrderNos().subList(offset, end);
|
||||
List<UnifiedOrderResponse.Row> rows = orders.platformRowsByOrderNos(batchOrderNos);
|
||||
if (rows.size() != batchOrderNos.size()) throw new BizException(ResultCode.NOT_FOUND, "所选订单不存在或不可见");
|
||||
boolean more = end < value.selectedOrderNos().size();
|
||||
return response(snapshot, rows, more ? String.valueOf(end) : null, more,
|
||||
offset == 0 ? (long) value.selectedOrderNos().size() : null);
|
||||
}
|
||||
|
||||
private ExportBatchResponse filtered(Validated value, String snapshot) {
|
||||
int page = cursor(value.cursor(), 1);
|
||||
UnifiedOrderQuery query = value.query(page);
|
||||
PageResult<UnifiedOrderResponse.Row> source = orders.platformPage(value.tenantId(), query);
|
||||
if (page == 1 && source.total() == 0) throw invalid("没有符合当前查询条件的可导出订单");
|
||||
if (page == 1 && source.total() > MAX_ROWS) throw invalid("导出订单数量超过最大限制 " + MAX_ROWS);
|
||||
boolean more = page < source.pages();
|
||||
return response(snapshot, source.records(), more ? String.valueOf(page + 1) : null, more,
|
||||
page == 1 ? source.total() : null);
|
||||
}
|
||||
|
||||
private ExportBatchResponse response(String snapshot, List<UnifiedOrderResponse.Row> source,
|
||||
String nextCursor, boolean hasMore, Long totalRows) {
|
||||
return new ExportBatchResponse(SCHEMA_VERSION, SCHEMA_HASH, snapshot,
|
||||
totalRows == null ? List.of() : COLUMNS, source.stream().map(this::row).toList(),
|
||||
nextCursor, hasMore, totalRows);
|
||||
}
|
||||
|
||||
private Map<String, Object> row(UnifiedOrderResponse.Row value) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("inboundCode", value.inboundCode()); row.put("orderNo", value.orderNo());
|
||||
row.put("mainStatus", value.mainStatus()); row.put("customerName", value.customerName());
|
||||
row.put("channelName", value.channelName()); row.put("receivingWarehouseName", value.receivingWarehouseName());
|
||||
row.put("destinationWarehouseName", value.destinationWarehouseName()); row.put("expectedShipDate", value.expectedShipDate());
|
||||
row.put("totalPackageCount", value.totalPackageCount()); row.put("totalQuantity", value.totalQuantity());
|
||||
row.put("actualGrossWeightKg", value.actualGrossWeightKg()); row.put("actualVolumeCbm", value.actualVolumeCbm());
|
||||
row.put("chargeableWeightKg", value.chargeableWeightKg()); row.put("chargeableVolumeCbm", value.chargeableVolumeCbm());
|
||||
row.put("feeReviewStatus", value.feeReviewStatus()); row.put("skuNos", join(value.skuNos()));
|
||||
row.put("taxMode", value.taxMode()); row.put("containerMode", value.containerMode());
|
||||
row.put("customsInspection", value.customsInspection());
|
||||
var transport = value.transport();
|
||||
row.put("containerNo", transport == null ? null : transport.containerNo());
|
||||
row.put("cabinetNo", transport == null ? null : transport.cabinetNo());
|
||||
row.put("vesselName", transport == null ? null : transport.vesselName());
|
||||
row.put("voyageNo", transport == null ? null : transport.voyageNo());
|
||||
row.put("createdAt", value.createdAt());
|
||||
return row;
|
||||
}
|
||||
|
||||
private Validated validate(ExportBatchRequest request) {
|
||||
if (request == null) throw invalid("Export batch request is required");
|
||||
positive(request.taskId(), "taskId"); positive(request.requestedBy(), "requestedBy");
|
||||
if (!"0".equals(request.tenantId()) || !"PLATFORM".equals(request.appCode())) {
|
||||
throw invalid("国内仓订单导出仅支持平台范围");
|
||||
}
|
||||
if (request.dispatchVersion() <= 0) throw invalid("dispatchVersion must be positive");
|
||||
if (request.limit() < 1 || request.limit() > MAX_BATCH_SIZE) throw invalid("limit must be between 1 and 500");
|
||||
Map<String, Object> parameters = request.parameters() == null ? Map.of() : request.parameters();
|
||||
if (!PARAMETER_KEYS.containsAll(parameters.keySet())) throw invalid("包含不支持的订单筛选字段");
|
||||
List<String> selected = selected(parameters.get("selectedOrderNos"));
|
||||
Long tenantId = positiveOptional(parameters.get("tenantId"), "tenantId");
|
||||
String snapshot = text(request.snapshotToken(), "snapshotToken", 512);
|
||||
if (snapshot == null && text(request.cursor(), "cursor", 32) != null) throw invalid("cursor requires snapshotToken");
|
||||
return new Validated(request, parameters, selected, tenantId, snapshot,
|
||||
text(request.cursor(), "cursor", 32), request.limit());
|
||||
}
|
||||
|
||||
private List<String> selected(Object raw) {
|
||||
if (raw == null) return List.of();
|
||||
if (!(raw instanceof Iterable<?> values)) throw invalid("selectedOrderNos 必须为数组");
|
||||
List<String> result = new ArrayList<>();
|
||||
for (Object value : values) {
|
||||
String orderNo = text(value, "selectedOrderNos", 64);
|
||||
if (orderNo == null) throw invalid("selectedOrderNos 不能包含空值");
|
||||
result.add(orderNo);
|
||||
if (result.size() > MAX_SELECTION) throw invalid("单次最多选择 " + MAX_SELECTION + " 张订单");
|
||||
}
|
||||
if (new LinkedHashSet<>(result).size() != result.size()) throw invalid("selectedOrderNos 不能重复");
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private String sign(Validated value) {
|
||||
return "v1." + Base64.getUrlEncoder().withoutPadding().encodeToString(hmac(subject(value)));
|
||||
}
|
||||
|
||||
private String verify(Validated value) {
|
||||
String[] parts = value.snapshotToken().split("\\.", -1);
|
||||
if (parts.length != 2 || !"v1".equals(parts[0])) throw invalid("snapshotToken is invalid");
|
||||
try {
|
||||
if (!MessageDigest.isEqual(hmac(subject(value)), Base64.getUrlDecoder().decode(parts[1]))) {
|
||||
throw invalid("snapshotToken does not match this export task");
|
||||
}
|
||||
} catch (IllegalArgumentException exception) { throw invalid("snapshotToken is invalid"); }
|
||||
return value.snapshotToken();
|
||||
}
|
||||
|
||||
private String subject(Validated value) {
|
||||
return String.join("\n", EXPORT_TYPE, value.source().taskId(), value.source().requestedBy(),
|
||||
String.valueOf(value.source().dispatchVersion()), canonical(value.parameters()));
|
||||
}
|
||||
|
||||
private String canonical(Map<String, Object> values) {
|
||||
return values.entrySet().stream().sorted(Map.Entry.comparingByKey())
|
||||
.map(entry -> entry.getKey() + "=" + String.valueOf(entry.getValue())).reduce("", (a, b) -> a + "\n" + b);
|
||||
}
|
||||
|
||||
private byte[] hmac(String value) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(signingKey, "HmacSHA256"));
|
||||
return mac.doFinal(value.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (GeneralSecurityException exception) { throw new IllegalStateException(exception); }
|
||||
}
|
||||
|
||||
private int cursor(String value, int defaultValue) {
|
||||
if (value == null) return defaultValue;
|
||||
try { int parsed = Integer.parseInt(value); if (parsed < 0) throw new NumberFormatException(); return parsed; }
|
||||
catch (NumberFormatException exception) { throw invalid("cursor must be a non-negative integer"); }
|
||||
}
|
||||
|
||||
private Long positiveOptional(Object value, String name) {
|
||||
if (value == null || String.valueOf(value).isBlank()) return null;
|
||||
try { long parsed = Long.parseLong(String.valueOf(value)); if (parsed <= 0) throw new NumberFormatException(); return parsed; }
|
||||
catch (NumberFormatException exception) { throw invalid(name + " 必须为正整数字符串"); }
|
||||
}
|
||||
|
||||
private void positive(String value, String name) { if (positiveOptional(value, name) == null) throw invalid(name + " is required"); }
|
||||
|
||||
private String text(Object value, String name, int max) {
|
||||
if (value == null) return null;
|
||||
if (!(value instanceof String raw)) throw invalid(name + " 必须为字符串");
|
||||
String normalized = raw.isBlank() ? null : raw.trim();
|
||||
if (normalized != null && normalized.length() > max) throw invalid(name + " 长度超过限制");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String join(List<String> values) { return values == null ? null : String.join(",", values); }
|
||||
private static ExportColumn column(String key, String title, ExportColumnType type, int width) {
|
||||
return new ExportColumn(key, title, type, width);
|
||||
}
|
||||
private static String schemaHash(List<ExportColumn> columns) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
for (ExportColumn column : columns) digest.update((column.key() + "\u001f" + column.title() + "\u001f"
|
||||
+ column.type() + "\u001f" + column.width() + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
return "sha256:" + java.util.HexFormat.of().formatHex(digest.digest());
|
||||
} catch (GeneralSecurityException exception) { throw new ExceptionInInitializerError(exception); }
|
||||
}
|
||||
private BizException invalid(String message) { return new BizException(ResultCode.VALIDATION_FAILED, message); }
|
||||
|
||||
private record Validated(ExportBatchRequest source, Map<String, Object> parameters,
|
||||
List<String> selectedOrderNos, Long tenantId, String snapshotToken,
|
||||
String cursor, int limit) {
|
||||
UnifiedOrderQuery query(int page) {
|
||||
return new UnifiedOrderQuery(string("keyword"), string("orderNo"), string("mainStatus"),
|
||||
number("customerId"), number("receivingWarehouseId"), number("destinationWarehouseId"),
|
||||
string("inboundCode"), string("internalOrderStatus"), date("expectedArrivalDate"),
|
||||
string("feeReviewStatus"), string("channelName"), string("containerNo"), string("cabinetNo"),
|
||||
date("sailingDate"), bool("loaded"), bool("fullContainer"), string("measurementReviewStatus"),
|
||||
string("fbaCode"), dateTime("createdFrom"), dateTime("createdTo"), page, limit);
|
||||
}
|
||||
private String string(String key) { Object value = parameters.get(key); return value == null || String.valueOf(value).isBlank() ? null : String.valueOf(value).trim(); }
|
||||
private Long number(String key) { String value = string(key); if (value == null) return null; try { long parsed=Long.parseLong(value); if(parsed<=0)throw new NumberFormatException(); return parsed; } catch(NumberFormatException ex){throw new BizException(ResultCode.VALIDATION_FAILED,key+" 必须为正整数字符串");} }
|
||||
private LocalDate date(String key) { String value=string(key); if(value==null)return null; try{return LocalDate.parse(value);}catch(Exception ex){throw new BizException(ResultCode.VALIDATION_FAILED,key+" 日期格式无效");} }
|
||||
private LocalDateTime dateTime(String key) { String value=string(key); if(value==null)return null; try{return LocalDateTime.parse(value);}catch(Exception ex){throw new BizException(ResultCode.VALIDATION_FAILED,key+" 时间格式无效");} }
|
||||
private Boolean bool(String key) { Object value=parameters.get(key); if(value==null)return null; if(value instanceof Boolean flag)return flag; if("true".equals(value)||"false".equals(value))return Boolean.valueOf(String.valueOf(value)); throw new BizException(ResultCode.VALIDATION_FAILED,key+" 必须为布尔值"); }
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,23 @@ public class UnifiedOrderQueryService {
|
||||
return page(tenantId, query, true);
|
||||
}
|
||||
|
||||
/** 平台导出选择项按业务订单号读取;不暴露内部 ID 作为选择契约。 */
|
||||
public List<UnifiedOrderResponse.Row> platformRowsByOrderNos(List<String> orderNos) {
|
||||
if (orderNos == null || orderNos.isEmpty()) return List.of();
|
||||
Map<String, OrderEntity> orderByNo = orders.findByOrderNos(orderNos).stream()
|
||||
.collect(Collectors.toMap(OrderEntity::getOrderNo, Function.identity(), (left, right) -> left));
|
||||
List<OrderEntity> selected = orderNos.stream().map(orderByNo::get).filter(Objects::nonNull).toList();
|
||||
List<Long> orderIds = selected.stream().map(OrderEntity::getId).toList();
|
||||
Map<Long, FirstlegUnifiedOrderClient.Summary> inboundByOrder = summaries(null,
|
||||
selected.stream().filter(order -> order.getStatus() != OrderStatus.SUBMITTED)
|
||||
.map(OrderEntity::getId).toList());
|
||||
Map<Long, String> latestNotes = latestNotes(null, orderIds);
|
||||
Map<Long, CargoSummary> cargoSummaries = cargoSummaries(null, orderIds);
|
||||
Map<Long, Integer> childCounts = childCounts(selected);
|
||||
return selected.stream().map(order -> row(order, inboundByOrder.get(order.getId()),
|
||||
latestNotes.get(order.getId()), childCounts, cargoSummaries.get(order.getId()), null)).toList();
|
||||
}
|
||||
|
||||
private PageResult<UnifiedOrderResponse.Row> page(Long tenantId, UnifiedOrderQuery query,
|
||||
boolean includeCoLoadGroups) {
|
||||
validate(query);
|
||||
@@ -163,7 +180,7 @@ public class UnifiedOrderQueryService {
|
||||
}
|
||||
|
||||
private Map<Long, FirstlegUnifiedOrderClient.Summary> summaries(Long tenantId, List<Long> orderIds) {
|
||||
if (orderIds.isEmpty()) return Map.of();
|
||||
if (orderIds == null || orderIds.isEmpty()) return Map.of();
|
||||
List<FirstlegUnifiedOrderClient.Summary> data = require(
|
||||
call(() -> firstleg.summaries(internalToken,
|
||||
new FirstlegUnifiedOrderClient.BatchRequest(tenantId, orderIds))),
|
||||
|
||||
Reference in New Issue
Block a user