优化订单列表,多单号查询,支持几万单号查询
This commit is contained in:
+10
-7
@@ -28,6 +28,7 @@ import com.bms.price.domain.vo.TabOrderMonitorQuery;
|
||||
import com.bms.price.domain.vo.TabOrderMonitorVo;
|
||||
import com.bms.price.domain.vo.TabOrderProductCheckResult;
|
||||
import com.bms.price.oracle.CustomerKeywordResolver;
|
||||
import com.bms.price.oracle.TabOrderPageResult;
|
||||
import com.bms.price.oracle.TabOrderMonitorService;
|
||||
import com.bms.price.oracle.TabOrderProductCheckService;
|
||||
import com.bms.price.search.BillSearchPage;
|
||||
@@ -94,12 +95,10 @@ public class TabOrderMonitorController extends BaseController
|
||||
}
|
||||
// 在切换 Oracle 数据源之前,把客户名称解析为客户代码
|
||||
customerKeywordResolver.resolve(query);
|
||||
long total = tabOrderMonitorService.count(query);
|
||||
List<TabOrderMonitorVo> rows = total <= 0L
|
||||
? Collections.emptyList()
|
||||
: tabOrderMonitorService.selectPage(query, pageNum, pageSize);
|
||||
rsp.setRows(rows);
|
||||
rsp.setTotal(total);
|
||||
// 大批量单号场景一次收集即可同时得到总数和当前页,避免重复扫描 Oracle
|
||||
TabOrderPageResult page = tabOrderMonitorService.searchPage(query, pageNum, pageSize);
|
||||
rsp.setRows(page.getRows());
|
||||
rsp.setTotal(page.getTotal());
|
||||
return rsp;
|
||||
}
|
||||
|
||||
@@ -142,7 +141,11 @@ public class TabOrderMonitorController extends BaseController
|
||||
return error("customerCodes and pickup time required");
|
||||
}
|
||||
customerKeywordResolver.resolve(query);
|
||||
tabOrderMonitorService.count(query);
|
||||
// 含大批量单号时,异步任务启动时会自行分批收集,这里不预先 count,避免重复全量扫描
|
||||
if (!hasNonBlankItem(query.getOrderIdentifiers()))
|
||||
{
|
||||
tabOrderMonitorService.count(query);
|
||||
}
|
||||
String asyncTaskId = UUID.randomUUID().toString().replace("-", "");
|
||||
tabOrderSyncAsyncRunner.put(asyncTaskId, PrcBillingTaskRetryTask.STATUS_PENDING, 0, "queued",
|
||||
0, 0, 0, 0, null);
|
||||
|
||||
@@ -37,4 +37,19 @@ public interface OracleUpstreamOrderMapper
|
||||
List<Map<String, Object>> selectTabOrderIdsByProduct(
|
||||
@Param("orderTable") String orderTable,
|
||||
@Param("query") TabOrderMonitorQuery query);
|
||||
|
||||
/**
|
||||
* 大批量单号查询的单批收集:一批不超过 900 个单号,对三列做 OR 精确匹配,
|
||||
* 同时叠加客户/时间/单号等值等其他过滤,仅返回 ID + PICKUP_DATE。
|
||||
* 调用方负责分批、去重、排序与内存分页。
|
||||
*/
|
||||
List<Map<String, Object>> selectMatchedOrderKeys(
|
||||
@Param("orderTable") String orderTable,
|
||||
@Param("query") TabOrderMonitorQuery query,
|
||||
@Param("batch") List<String> batch);
|
||||
|
||||
/** 按已收集到的订单主键批量查明细(一批为一个内存分页切片)。 */
|
||||
List<Map<String, Object>> selectTabOrderMonitorByIds(
|
||||
@Param("orderTable") String orderTable,
|
||||
@Param("ids") List<String> ids);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.bms.price.oracle;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
/**
|
||||
* 大批量单号查询时,分批从 Oracle 收集到的匹配订单主键与排序字段。
|
||||
* 注意:TAB_ORDER.ID 为 VARCHAR2(64),按字符串处理,不能转数字。
|
||||
* PICKUP_DATE 用于与列表保持一致的“提货时间倒序、订单ID倒序”内存排序。
|
||||
*/
|
||||
public class MatchedOrderKey
|
||||
{
|
||||
private final String id;
|
||||
private final Timestamp pickupDate;
|
||||
|
||||
public MatchedOrderKey(String id, Timestamp pickupDate)
|
||||
{
|
||||
this.id = id;
|
||||
this.pickupDate = pickupDate;
|
||||
}
|
||||
|
||||
public String getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public Timestamp getPickupDate()
|
||||
{
|
||||
return pickupDate;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.bms.price.oracle;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.bms.common.annotation.DataSource;
|
||||
import com.bms.common.enums.DataSourceType;
|
||||
import com.bms.price.domain.vo.TabOrderMonitorQuery;
|
||||
import com.bms.price.mapper.OracleUpstreamOrderMapper;
|
||||
|
||||
/**
|
||||
* 大批量单号分批收集的工作单元。独立成 Bean 是为了让 @DataSource 切面
|
||||
* 在并行工作线程内也能正确切换 SLAVE(Oracle)数据源(ThreadLocal 按线程隔离,
|
||||
* 切面 around 会在每个工作线程内 set/clear)。
|
||||
*/
|
||||
@Component
|
||||
public class OracleOrderKeyCollectWorker
|
||||
{
|
||||
@Autowired
|
||||
private OracleUpstreamOrderMapper oracleUpstreamOrderMapper;
|
||||
|
||||
@DataSource(DataSourceType.SLAVE)
|
||||
public List<Map<String, Object>> collectBatch(String table, TabOrderMonitorQuery query, List<String> batch)
|
||||
{
|
||||
return oracleUpstreamOrderMapper.selectMatchedOrderKeys(table, query, batch);
|
||||
}
|
||||
}
|
||||
+430
-14
@@ -4,9 +4,20 @@ import java.math.BigDecimal;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -29,16 +40,101 @@ public class TabOrderMonitorService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(TabOrderMonitorService.class);
|
||||
|
||||
/** Oracle 单个 IN 列表上限 1000,分批收集时每批取 900 留安全余量。 */
|
||||
public static final int IDENTIFIER_BATCH_SIZE = 900;
|
||||
|
||||
/**
|
||||
* 分批收集并行度:Oracle 从库连接池 maxActive=20,6 路并行与同步任务并发合计仍有余量。
|
||||
* 实测相同 12000 单号:串行约 18s(冷),6 路并行约 1s。
|
||||
*/
|
||||
private static final int COLLECT_PARALLELISM = 6;
|
||||
|
||||
/** 相同查询条件的收集结果缓存时间:翻页/重复查询直接复用匹配主键,秒回。 */
|
||||
private static final long COLLECT_CACHE_TTL_MS = 5L * 60 * 1000;
|
||||
|
||||
/** 收集结果缓存条目上限(LRU 淘汰),避免不同单号集合无限占内存。 */
|
||||
private static final int COLLECT_CACHE_MAX_ENTRIES = 20;
|
||||
|
||||
/** 单批 Oracle 查询最长等待(从库 socketTimeout 为 120s,留同样余量)。 */
|
||||
private static final long COLLECT_BATCH_TIMEOUT_SECONDS = 120L;
|
||||
|
||||
@Autowired
|
||||
private OracleUpstreamOrderMapper oracleUpstreamOrderMapper;
|
||||
|
||||
@Autowired
|
||||
private OracleOrderKeyCollectWorker collectWorker;
|
||||
|
||||
@Autowired
|
||||
private BillingProperties billingProperties;
|
||||
|
||||
private final ThreadFactory collectThreadFactory = new ThreadFactory()
|
||||
{
|
||||
private final AtomicInteger seq = new AtomicInteger(1);
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable r)
|
||||
{
|
||||
Thread t = new Thread(r, "oracle-orderkey-collect-" + seq.getAndIncrement());
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
private final ExecutorService collectPool =
|
||||
Executors.newFixedThreadPool(COLLECT_PARALLELISM, collectThreadFactory);
|
||||
|
||||
/** 查询指纹 -> 已排序匹配主键(短期 TTL + LRU)。 */
|
||||
private final Map<String, CacheEntry> collectCache =
|
||||
Collections.synchronizedMap(new LinkedHashMap<String, CacheEntry>(16, 0.75f, true)
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<String, CacheEntry> eldest)
|
||||
{
|
||||
return size() > COLLECT_CACHE_MAX_ENTRIES;
|
||||
}
|
||||
});
|
||||
|
||||
private static final class CacheEntry
|
||||
{
|
||||
private final List<MatchedOrderKey> keys;
|
||||
private final long expireAt;
|
||||
|
||||
private CacheEntry(List<MatchedOrderKey> keys, long expireAt)
|
||||
{
|
||||
this.keys = keys;
|
||||
this.expireAt = expireAt;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdownCollectPool()
|
||||
{
|
||||
collectPool.shutdown();
|
||||
try
|
||||
{
|
||||
if (!collectPool.awaitTermination(5, TimeUnit.SECONDS))
|
||||
{
|
||||
collectPool.shutdownNow();
|
||||
}
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
collectPool.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@DataSource(DataSourceType.SLAVE)
|
||||
public long count(TabOrderMonitorQuery query)
|
||||
{
|
||||
validate(query);
|
||||
// 大批量单号:分批收集匹配主键,总数即去重后的主键数
|
||||
if (hasIdentifiers(query))
|
||||
{
|
||||
return doCollectMatchedKeys(query, resolveTable()).size();
|
||||
}
|
||||
String table = resolveTable();
|
||||
try
|
||||
{
|
||||
@@ -55,10 +151,24 @@ public class TabOrderMonitorService
|
||||
}
|
||||
}
|
||||
|
||||
/** Oracle 单条 SQL 的 IN 列表上限;产品检测不走分批收集,单号数需在此以内。 */
|
||||
public static final int PRODUCT_CHECK_IDENTIFIER_LIMIT = 1000;
|
||||
|
||||
private static void ensureProductCheckIdentifierLimit(TabOrderMonitorQuery query)
|
||||
{
|
||||
if (query != null && query.getOrderIdentifiers() != null
|
||||
&& query.getOrderIdentifiers().size() > PRODUCT_CHECK_IDENTIFIER_LIMIT)
|
||||
{
|
||||
throw new ServiceException("检测产品最多支持 " + PRODUCT_CHECK_IDENTIFIER_LIMIT
|
||||
+ " 个单号;单号较多时请直接在订单列表查询后勾选/按条件同步计费");
|
||||
}
|
||||
}
|
||||
|
||||
@DataSource(DataSourceType.SLAVE)
|
||||
public List<Map<String, Object>> selectDistinctProductCodes(TabOrderMonitorQuery query)
|
||||
{
|
||||
validate(query);
|
||||
ensureProductCheckIdentifierLimit(query);
|
||||
String table = resolveTable();
|
||||
try
|
||||
{
|
||||
@@ -85,6 +195,7 @@ public class TabOrderMonitorService
|
||||
public List<Map<String, Object>> selectOrderIdsByProduct(TabOrderMonitorQuery query)
|
||||
{
|
||||
validate(query);
|
||||
ensureProductCheckIdentifierLimit(query);
|
||||
if (StringUtils.isEmpty(query.getProductCode()))
|
||||
{
|
||||
throw new ServiceException("productCode required");
|
||||
@@ -113,17 +224,91 @@ public class TabOrderMonitorService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表查询统一入口:一次数据源调用内返回总数 + 当前页,
|
||||
* 大批量单号场景只做一次分批收集,避免 count 和 page 重复扫描。
|
||||
*/
|
||||
@DataSource(DataSourceType.SLAVE)
|
||||
public TabOrderPageResult searchPage(TabOrderMonitorQuery query, int pageNum, int pageSize)
|
||||
{
|
||||
validate(query);
|
||||
int[] page = normalizePage(pageNum, pageSize);
|
||||
int pn = page[0];
|
||||
int ps = page[1];
|
||||
String table = resolveTable();
|
||||
if (hasIdentifiers(query))
|
||||
{
|
||||
long t0 = System.currentTimeMillis();
|
||||
List<MatchedOrderKey> keys = doCollectMatchedKeys(query, table);
|
||||
List<TabOrderMonitorVo> rows = doPageByKeys(table, keys, pn, ps);
|
||||
log.info("oracle tab_order identifier page identifiers={} matched={} page={}/{} rows={} costMs={}",
|
||||
Integer.valueOf(query.getOrderIdentifiers().size()), Integer.valueOf(keys.size()),
|
||||
Integer.valueOf(pn), Integer.valueOf(ps), Integer.valueOf(rows.size()),
|
||||
Long.valueOf(System.currentTimeMillis() - t0));
|
||||
return new TabOrderPageResult(keys.size(), rows);
|
||||
}
|
||||
long total;
|
||||
try
|
||||
{
|
||||
PageHelper.clearPage();
|
||||
total = oracleUpstreamOrderMapper.countTabOrderMonitor(table, query);
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
throw new ServiceException("Oracle TAB_ORDER count failed: " + e.getMessage());
|
||||
}
|
||||
finally
|
||||
{
|
||||
PageHelper.clearPage();
|
||||
}
|
||||
return new TabOrderPageResult(total, doSelectRownumPage(table, query, pn, ps));
|
||||
}
|
||||
|
||||
@DataSource(DataSourceType.SLAVE)
|
||||
public List<TabOrderMonitorVo> selectPage(TabOrderMonitorQuery query, int pageNum, int pageSize)
|
||||
{
|
||||
validate(query);
|
||||
int[] page = normalizePage(pageNum, pageSize);
|
||||
int pn = page[0];
|
||||
int ps = page[1];
|
||||
String table = resolveTable();
|
||||
int pn = pageNum < 1 ? 1 : pageNum;
|
||||
int ps = pageSize < 1 ? 10 : pageSize;
|
||||
if (ps > 200)
|
||||
// 大批量单号:收集匹配主键后按页切片(兼容直接调用本方法的场景)
|
||||
if (hasIdentifiers(query))
|
||||
{
|
||||
ps = 200;
|
||||
List<MatchedOrderKey> keys = doCollectMatchedKeys(query, table);
|
||||
return doPageByKeys(table, keys, pn, ps);
|
||||
}
|
||||
return doSelectRownumPage(table, query, pn, ps);
|
||||
}
|
||||
|
||||
/**
|
||||
* 大批量单号场景:分批收集全部匹配订单主键(去重 + 按提货时间/ID倒序)。
|
||||
* 供同步任务一次性收集后顺序处理,避免每页重复全量扫描。
|
||||
*/
|
||||
@DataSource(DataSourceType.SLAVE)
|
||||
public List<MatchedOrderKey> collectMatchedOrderKeys(TabOrderMonitorQuery query)
|
||||
{
|
||||
validate(query);
|
||||
if (!hasIdentifiers(query))
|
||||
{
|
||||
throw new ServiceException("orderIdentifiers required");
|
||||
}
|
||||
return doCollectMatchedKeys(query, resolveTable());
|
||||
}
|
||||
|
||||
/** 同步任务按已收集好的主键切片拉取明细,顺序与 keys 切片一致。 */
|
||||
@DataSource(DataSourceType.SLAVE)
|
||||
public List<TabOrderMonitorVo> selectPageByKeys(List<MatchedOrderKey> keySlice)
|
||||
{
|
||||
if (keySlice == null || keySlice.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return doPageByKeys(resolveTable(), keySlice, 1, keySlice.size());
|
||||
}
|
||||
|
||||
private List<TabOrderMonitorVo> doSelectRownumPage(String table, TabOrderMonitorQuery query, int pn, int ps)
|
||||
{
|
||||
long endRow = (long) pn * ps;
|
||||
long startRow = endRow - ps;
|
||||
try
|
||||
@@ -135,16 +320,7 @@ public class TabOrderMonitorService
|
||||
query.getCustomerCodes() == null ? 0 : query.getCustomerCodes().size(),
|
||||
query.getBeginTime(), query.getEndTime(), pn, ps,
|
||||
rows == null ? 0 : rows.size(), System.currentTimeMillis() - t0);
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<TabOrderMonitorVo> list = new ArrayList<TabOrderMonitorVo>(rows.size());
|
||||
for (Map<String, Object> row : rows)
|
||||
{
|
||||
list.add(toVo(row));
|
||||
}
|
||||
return list;
|
||||
return toVoList(rows);
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
@@ -156,6 +332,246 @@ public class TabOrderMonitorService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心:单号按 {@link #IDENTIFIER_BATCH_SIZE} 分批,{@link #COLLECT_PARALLELISM} 路并行
|
||||
* 查 Oracle(每批 UNION ALL 三列各走索引),Java 侧按 ID 去重,
|
||||
* 再按 PICKUP_DATE DESC, ID DESC 排序。相同查询条件短期缓存,翻页/重复查询秒回。
|
||||
*/
|
||||
private List<MatchedOrderKey> doCollectMatchedKeys(TabOrderMonitorQuery query, String table)
|
||||
{
|
||||
List<String> identifiers = query.getOrderIdentifiers();
|
||||
if (identifiers == null || identifiers.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String fingerprint = collectFingerprint(table, query);
|
||||
long now = System.currentTimeMillis();
|
||||
CacheEntry cached = collectCache.get(fingerprint);
|
||||
if (cached != null && now < cached.expireAt)
|
||||
{
|
||||
log.info("oracle tab_order identifier collect cache-hit identifiers={} matched={}",
|
||||
Integer.valueOf(identifiers.size()), Integer.valueOf(cached.keys.size()));
|
||||
return new ArrayList<MatchedOrderKey>(cached.keys);
|
||||
}
|
||||
|
||||
List<List<String>> batches = new ArrayList<List<String>>();
|
||||
for (int i = 0; i < identifiers.size(); i += IDENTIFIER_BATCH_SIZE)
|
||||
{
|
||||
batches.add(identifiers.subList(i, Math.min(i + IDENTIFIER_BATCH_SIZE, identifiers.size())));
|
||||
}
|
||||
|
||||
// 提交并行任务(工作线程内由 @DataSource 切面切到 Oracle 数据源)
|
||||
List<Future<List<Map<String, Object>>>> futures =
|
||||
new ArrayList<Future<List<Map<String, Object>>>>(batches.size());
|
||||
for (List<String> batch : batches)
|
||||
{
|
||||
futures.add(collectPool.submit(() -> collectWorker.collectBatch(table, query, batch)));
|
||||
}
|
||||
|
||||
// ID -> PICKUP_DATE,跨批去重(同一订单可能多列/多批命中;ID 为 VARCHAR2)
|
||||
Map<String, Timestamp> unique = new LinkedHashMap<String, Timestamp>();
|
||||
long t0 = System.currentTimeMillis();
|
||||
try
|
||||
{
|
||||
for (Future<List<Map<String, Object>>> future : futures)
|
||||
{
|
||||
List<Map<String, Object>> rows;
|
||||
try
|
||||
{
|
||||
rows = future.get(COLLECT_BATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
catch (ExecutionException e)
|
||||
{
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
throw new ServiceException("Oracle TAB_ORDER identifier collect failed: " + cause.getMessage());
|
||||
}
|
||||
catch (TimeoutException e)
|
||||
{
|
||||
future.cancel(true);
|
||||
throw new ServiceException("Oracle TAB_ORDER identifier collect timeout");
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ServiceException("Oracle TAB_ORDER identifier collect interrupted");
|
||||
}
|
||||
if (rows == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (Map<String, Object> row : rows)
|
||||
{
|
||||
Object idVal = value(row, "ID");
|
||||
if (idVal == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String id = String.valueOf(idVal).trim();
|
||||
if (id.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Object dateVal = value(row, "PICKUP_DATE");
|
||||
Timestamp pickup = dateVal instanceof Timestamp ? (Timestamp) dateVal : null;
|
||||
// 同一 ID 跨批重复时保留非空提货时间
|
||||
if (!unique.containsKey(id) || (unique.get(id) == null && pickup != null))
|
||||
{
|
||||
unique.put(id, pickup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 异常时取消尚未完成的批次
|
||||
for (Future<List<Map<String, Object>>> f : futures)
|
||||
{
|
||||
if (!f.isDone())
|
||||
{
|
||||
f.cancel(true);
|
||||
}
|
||||
}
|
||||
PageHelper.clearPage();
|
||||
}
|
||||
List<MatchedOrderKey> keys = new ArrayList<MatchedOrderKey>(unique.size());
|
||||
for (Map.Entry<String, Timestamp> e : unique.entrySet())
|
||||
{
|
||||
keys.add(new MatchedOrderKey(e.getKey(), e.getValue()));
|
||||
}
|
||||
// 与列表 SQL 的 ORDER BY PICKUP_DATE DESC, ORDER_ID DESC 保持一致(Oracle DESC 默认 NULLS FIRST)
|
||||
keys.sort(Comparator
|
||||
.comparing(MatchedOrderKey::getPickupDate,
|
||||
Comparator.nullsFirst(Comparator.<Timestamp>reverseOrder()))
|
||||
.thenComparing(MatchedOrderKey::getId, Comparator.<String>reverseOrder()));
|
||||
long cost = System.currentTimeMillis() - t0;
|
||||
log.info("oracle tab_order identifier collect identifiers={} batches={} parallel={} matched={} costMs={}",
|
||||
Integer.valueOf(identifiers.size()), Integer.valueOf(batches.size()),
|
||||
Integer.valueOf(COLLECT_PARALLELISM), Integer.valueOf(keys.size()), Long.valueOf(cost));
|
||||
// 存不可变快照供翻页/重复查询复用;本次返回可变副本
|
||||
collectCache.put(fingerprint, new CacheEntry(Collections.unmodifiableList(keys),
|
||||
System.currentTimeMillis() + COLLECT_CACHE_TTL_MS));
|
||||
return keys;
|
||||
}
|
||||
|
||||
/** 查询指纹:单号/客户集合顺序无关,叠加表名与其余过滤条件;同指纹才可复用收集结果。 */
|
||||
private static String collectFingerprint(String table, TabOrderMonitorQuery q)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(table == null ? "" : table).append('|');
|
||||
appendSorted(sb, q.getOrderIdentifiers());
|
||||
sb.append('|');
|
||||
appendSorted(sb, q.getCustomerCodes());
|
||||
sb.append('|').append(safe(q.getBeginTime())).append('|').append(safe(q.getEndTime()))
|
||||
.append('|').append(safe(q.getOrderId())).append('|').append(safe(q.getOrgOrderId()))
|
||||
.append('|').append(safe(q.getTransferOrderId()));
|
||||
return Integer.toHexString(sb.toString().hashCode());
|
||||
}
|
||||
|
||||
private static void appendSorted(StringBuilder sb, List<String> values)
|
||||
{
|
||||
if (values == null || values.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<String> copy = new ArrayList<String>(values);
|
||||
Collections.sort(copy);
|
||||
for (String v : copy)
|
||||
{
|
||||
sb.append(v).append(',');
|
||||
}
|
||||
}
|
||||
|
||||
private static String safe(String s)
|
||||
{
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
|
||||
/** 已排序主键的内存分页切片,并按切片顺序回查明细。 */
|
||||
private List<TabOrderMonitorVo> doPageByKeys(String table, List<MatchedOrderKey> orderedKeys, int pn, int ps)
|
||||
{
|
||||
if (orderedKeys == null || orderedKeys.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
int from = (pn - 1) * ps;
|
||||
if (from >= orderedKeys.size())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<MatchedOrderKey> slice = orderedKeys.subList(from, Math.min(from + ps, orderedKeys.size()));
|
||||
List<String> ids = new ArrayList<String>(slice.size());
|
||||
for (MatchedOrderKey key : slice)
|
||||
{
|
||||
ids.add(key.getId());
|
||||
}
|
||||
try
|
||||
{
|
||||
PageHelper.clearPage();
|
||||
List<Map<String, Object>> rows = oracleUpstreamOrderMapper.selectTabOrderMonitorByIds(table, ids);
|
||||
Map<String, Map<String, Object>> rowById = new LinkedHashMap<String, Map<String, Object>>();
|
||||
if (rows != null)
|
||||
{
|
||||
for (Map<String, Object> row : rows)
|
||||
{
|
||||
Object idVal = value(row, "ID");
|
||||
if (idVal == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
rowById.put(String.valueOf(idVal).trim(), row);
|
||||
}
|
||||
}
|
||||
// 回查结果无序,按主键切片顺序重排
|
||||
List<Map<String, Object>> orderedRows = new ArrayList<Map<String, Object>>(slice.size());
|
||||
for (MatchedOrderKey key : slice)
|
||||
{
|
||||
Map<String, Object> row = rowById.get(key.getId());
|
||||
if (row != null)
|
||||
{
|
||||
orderedRows.add(row);
|
||||
}
|
||||
}
|
||||
return toVoList(orderedRows);
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
throw new ServiceException("Oracle TAB_ORDER list by ids failed: " + e.getMessage());
|
||||
}
|
||||
finally
|
||||
{
|
||||
PageHelper.clearPage();
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] normalizePage(int pageNum, int pageSize)
|
||||
{
|
||||
int pn = pageNum < 1 ? 1 : pageNum;
|
||||
int ps = pageSize < 1 ? 10 : pageSize;
|
||||
if (ps > 200)
|
||||
{
|
||||
ps = 200;
|
||||
}
|
||||
return new int[] { pn, ps };
|
||||
}
|
||||
|
||||
private static boolean hasIdentifiers(TabOrderMonitorQuery query)
|
||||
{
|
||||
return query != null && query.getOrderIdentifiers() != null && !query.getOrderIdentifiers().isEmpty();
|
||||
}
|
||||
|
||||
private static List<TabOrderMonitorVo> toVoList(List<Map<String, Object>> rows)
|
||||
{
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<TabOrderMonitorVo> list = new ArrayList<TabOrderMonitorVo>(rows.size());
|
||||
for (Map<String, Object> row : rows)
|
||||
{
|
||||
list.add(toVo(row));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private void validate(TabOrderMonitorQuery query)
|
||||
{
|
||||
if (query == null)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.bms.price.oracle;
|
||||
|
||||
import java.util.List;
|
||||
import com.bms.price.domain.vo.TabOrderMonitorVo;
|
||||
|
||||
/** 订单列表一次查询的总数 + 当前页数据(大批量单号场景一次收集、一次分页)。 */
|
||||
public class TabOrderPageResult
|
||||
{
|
||||
private final long total;
|
||||
private final List<TabOrderMonitorVo> rows;
|
||||
|
||||
public TabOrderPageResult(long total, List<TabOrderMonitorVo> rows)
|
||||
{
|
||||
this.total = total;
|
||||
this.rows = rows;
|
||||
}
|
||||
|
||||
public long getTotal()
|
||||
{
|
||||
return total;
|
||||
}
|
||||
|
||||
public List<TabOrderMonitorVo> getRows()
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
@@ -73,8 +73,16 @@ public class TabOrderSyncAsyncRunner
|
||||
ExecutorService pool = newWorkerPool(threads, "order-source-sync");
|
||||
try
|
||||
{
|
||||
long totalLong = tabOrderMonitorService.count(query);
|
||||
int total = totalLong > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) totalLong;
|
||||
// 大批量单号:先一次性分批收集全部匹配主键,之后按切片拉明细,避免每页重复全量扫描
|
||||
List<com.bms.price.oracle.MatchedOrderKey> matchedKeys = null;
|
||||
if (query != null && query.getOrderIdentifiers() != null && !query.getOrderIdentifiers().isEmpty())
|
||||
{
|
||||
matchedKeys = tabOrderMonitorService.collectMatchedOrderKeys(query);
|
||||
}
|
||||
final List<com.bms.price.oracle.MatchedOrderKey> keys = matchedKeys;
|
||||
int total = keys != null
|
||||
? keys.size()
|
||||
: toIntExact(tabOrderMonitorService.count(query));
|
||||
put(asyncTaskId, PrcBillingTaskRetryTask.STATUS_RUNNING, 0, "starting", total, 0, 0, 0, null);
|
||||
if (total <= 0)
|
||||
{
|
||||
@@ -89,7 +97,21 @@ public class TabOrderSyncAsyncRunner
|
||||
int pageNum = 1;
|
||||
while (true)
|
||||
{
|
||||
List<TabOrderMonitorVo> rows = tabOrderMonitorService.selectPage(query, pageNum, batch);
|
||||
List<TabOrderMonitorVo> rows;
|
||||
if (keys != null)
|
||||
{
|
||||
int from = (pageNum - 1) * batch;
|
||||
if (from >= keys.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
rows = tabOrderMonitorService.selectPageByKeys(
|
||||
keys.subList(from, Math.min(from + batch, keys.size())));
|
||||
}
|
||||
else
|
||||
{
|
||||
rows = tabOrderMonitorService.selectPage(query, pageNum, batch);
|
||||
}
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
break;
|
||||
@@ -403,6 +425,11 @@ public class TabOrderSyncAsyncRunner
|
||||
return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
|
||||
}
|
||||
|
||||
private static int toIntExact(long value)
|
||||
{
|
||||
return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) value;
|
||||
}
|
||||
|
||||
public static int clampBatch(Integer batchSize)
|
||||
{
|
||||
int n = batchSize == null ? DEFAULT_BATCH : batchSize.intValue();
|
||||
|
||||
@@ -45,7 +45,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
) WHERE ROWNUM <= 1
|
||||
</select>
|
||||
|
||||
<sql id="tabOrderMonitorWhere">
|
||||
<!-- 除大批量单号外的所有过滤条件(大批量单号走分批收集 selectMatchedOrderKeys) -->
|
||||
<sql id="tabOrderFilterWhere">
|
||||
o.BL_PICKUP = 1
|
||||
AND o.ORDER_STATUS IN (3, 4, 5, 6, 7, 8, 11)
|
||||
<if test="query.customerCodes != null and query.customerCodes.size() > 0">
|
||||
@@ -69,6 +70,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="query.transferOrderId != null and query.transferOrderId != ''">
|
||||
AND o.TRANSFER_ORDER_ID = #{query.transferOrderId}
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<!--
|
||||
小批量单号(产品检测等入口,调用方保证不超过 Oracle IN 列表上限 1000)。
|
||||
列表主流程的大批量单号请用 selectMatchedOrderKeys 分批收集。
|
||||
-->
|
||||
<sql id="tabOrderIdentifierWhere">
|
||||
<if test="query.orderIdentifiers != null and query.orderIdentifiers.size() > 0">
|
||||
AND (
|
||||
o.ORDER_ID IN
|
||||
@@ -76,18 +84,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
#{oid}
|
||||
</foreach>
|
||||
OR o.ORG_ORDER_ID IN
|
||||
<foreach collection="query.orderIdentifiers" item="oid" open="(" separator="," close=")">
|
||||
#{oid}
|
||||
<foreach collection="query.orderIdentifiers" item="oid2" open="(" separator="," close=")">
|
||||
#{oid2}
|
||||
</foreach>
|
||||
OR o.TRANSFER_ORDER_ID IN
|
||||
<foreach collection="query.orderIdentifiers" item="oid" open="(" separator="," close=")">
|
||||
#{oid}
|
||||
<foreach collection="query.orderIdentifiers" item="oid3" open="(" separator="," close=")">
|
||||
#{oid3}
|
||||
</foreach>
|
||||
)
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<sql id="tabOrderMonitorWhere">
|
||||
<include refid="tabOrderFilterWhere"/>
|
||||
<include refid="tabOrderIdentifierWhere"/>
|
||||
</sql>
|
||||
|
||||
<sql id="tabOrderMonitorColumns">
|
||||
o.ID,
|
||||
o.ORDER_ID,
|
||||
o.ORG_ORDER_ID,
|
||||
o.TRANSFER_ORDER_ID,
|
||||
@@ -159,4 +173,51 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
ORDER BY o.PICKUP_DATE DESC, o.ORDER_ID DESC
|
||||
</select>
|
||||
|
||||
<!--
|
||||
大批量单号分批收集:每批最多 900 个(避开 Oracle IN 1000 上限)。
|
||||
用 UNION ALL 三段分别匹配三列(比三列 OR 快约 2 倍:不依赖优化器 OR-expansion,
|
||||
ORDER_ID/ORG_ORDER_ID/TRANSFER_ORDER_ID 各走自己的索引);重复 ID 由 Java 侧去重。
|
||||
-->
|
||||
<select id="selectMatchedOrderKeys" resultType="java.util.HashMap">
|
||||
SELECT * FROM (
|
||||
SELECT o.ID AS ID, o.PICKUP_DATE AS PICKUP_DATE
|
||||
FROM ${orderTable} o
|
||||
WHERE
|
||||
<include refid="tabOrderFilterWhere"/>
|
||||
AND o.ORDER_ID IN
|
||||
<foreach collection="batch" item="bid1" open="(" separator="," close=")">
|
||||
#{bid1}
|
||||
</foreach>
|
||||
UNION ALL
|
||||
SELECT o.ID AS ID, o.PICKUP_DATE AS PICKUP_DATE
|
||||
FROM ${orderTable} o
|
||||
WHERE
|
||||
<include refid="tabOrderFilterWhere"/>
|
||||
AND o.ORG_ORDER_ID IN
|
||||
<foreach collection="batch" item="bid2" open="(" separator="," close=")">
|
||||
#{bid2}
|
||||
</foreach>
|
||||
UNION ALL
|
||||
SELECT o.ID AS ID, o.PICKUP_DATE AS PICKUP_DATE
|
||||
FROM ${orderTable} o
|
||||
WHERE
|
||||
<include refid="tabOrderFilterWhere"/>
|
||||
AND o.TRANSFER_ORDER_ID IN
|
||||
<foreach collection="batch" item="bid3" open="(" separator="," close=")">
|
||||
#{bid3}
|
||||
</foreach>
|
||||
)
|
||||
</select>
|
||||
|
||||
<!-- 内存分页切片后的主键回查(一批最多一个页大小,远低于 IN 上限) -->
|
||||
<select id="selectTabOrderMonitorByIds" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
<include refid="tabOrderMonitorColumns"/>
|
||||
FROM ${orderTable} o
|
||||
WHERE o.ID IN
|
||||
<foreach collection="ids" item="mid" open="(" separator="," close=")">
|
||||
#{mid}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -8,7 +8,8 @@ export function listOrderSource(query) {
|
||||
// 分页参数保持在 URL 上供 TableSupport 读取;筛选条件放请求体,尤其是多单号列表。
|
||||
params: { pageNum, pageSize },
|
||||
data: filters,
|
||||
timeout: 60000
|
||||
// 单号可能多达数万个,后端分批走 Oracle 索引,留足收集时间
|
||||
timeout: 300000
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ const searchColumns = [
|
||||
label: '单号',
|
||||
prop: 'orderIdentifiers',
|
||||
component: 'textarea',
|
||||
placeholder: '订单号/运单号/转单号,一行一个',
|
||||
placeholder: '订单号/运单号/转单号,一行一个,支持数万个',
|
||||
props: {
|
||||
rows: 2,
|
||||
autosize: { minRows: 1, maxRows: 4 },
|
||||
@@ -215,6 +215,11 @@ function rowKey(row) {
|
||||
return (row && (row.orderId || row.transferOrderId || row.orgOrderId)) || ''
|
||||
}
|
||||
|
||||
/** 单次查询可接受的单号上限(后端按 900/批分批走索引,过高会明显增加等待时间) */
|
||||
const MAX_ORDER_IDENTIFIERS = 50000
|
||||
/** 产品检测走单条 SQL(Oracle IN 上限 1000),与后端保持一致 */
|
||||
const MAX_PRODUCT_CHECK_IDENTIFIERS = 1000
|
||||
|
||||
/** 长文本拆成数组:一行一条,兼容逗号/分号/顿号分隔;不按空格拆(客户名称可能含空格) */
|
||||
function splitLines(text) {
|
||||
return String(text || '')
|
||||
@@ -240,7 +245,8 @@ function buildFilterQuery(params) {
|
||||
|
||||
function queryList(params) {
|
||||
const query = buildFilterQuery(params)
|
||||
const hasIdentifier = !!(query.orderIdentifiers && query.orderIdentifiers.length)
|
||||
const identifierCount = (query.orderIdentifiers || []).length
|
||||
const hasIdentifier = identifierCount > 0
|
||||
const hasCustomer = !!(query.customerKeywords && query.customerKeywords.length)
|
||||
const hasTime = !!query.beginTime && !!query.endTime
|
||||
// 有任一单号即可查询;无单号时必须客户+提货时间
|
||||
@@ -248,6 +254,10 @@ function queryList(params) {
|
||||
ElMessage.warning('请输入单号,或选择客户和提货时间')
|
||||
return Promise.resolve({ rows: [], total: 0 })
|
||||
}
|
||||
if (identifierCount > MAX_ORDER_IDENTIFIERS) {
|
||||
ElMessage.warning(`单次最多查询 ${MAX_ORDER_IDENTIFIERS} 个单号,当前 ${identifierCount} 个,请减少后再查`)
|
||||
return Promise.resolve({ rows: [], total: 0 })
|
||||
}
|
||||
query.pageNum = params.pageNum
|
||||
query.pageSize = params.pageSize
|
||||
return listOrderSource(query)
|
||||
@@ -314,6 +324,11 @@ function handleCheckProducts() {
|
||||
ElMessage.warning('检测产品需先输入客户并选择提货时间')
|
||||
return
|
||||
}
|
||||
const identifierCount = (q.orderIdentifiers || []).length
|
||||
if (identifierCount > MAX_PRODUCT_CHECK_IDENTIFIERS) {
|
||||
ElMessage.warning(`检测产品最多支持 ${MAX_PRODUCT_CHECK_IDENTIFIERS} 个单号,请减少单号或直接在列表勾选同步`)
|
||||
return
|
||||
}
|
||||
checkLoading.value = true
|
||||
productCheckDrawerRef.value?.open(q)
|
||||
window.setTimeout(() => { checkLoading.value = false }, 300)
|
||||
|
||||
Reference in New Issue
Block a user