订单数据源增加订单对比功能

This commit is contained in:
wuweihua
2026-10-20 13:55:55 +08:00
parent 8093cb52e7
commit 45969a4d29
20 changed files with 1865 additions and 96 deletions
@@ -0,0 +1,37 @@
package com.bms.price.domain.vo;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* Order-bill product check: customer package (sales) products vs product catalog.
*/
@Data
public class OrderBillProductCheckResult implements Serializable
{
private static final long serialVersionUID = 1L;
/** Orders matched by search filters (customer + billing time). */
private long orderTotal;
private int packageProductCount;
private int matchedCount;
private int missingCount;
private String asOfTime;
private List<ProductRow> packageProducts = new ArrayList<ProductRow>();
private List<ProductRow> missingProducts = new ArrayList<ProductRow>();
@Data
public static class ProductRow implements Serializable
{
private static final long serialVersionUID = 1L;
private String customerCode;
private String productCode;
private String productName;
/** Whether product exists in catalog (effective at asOf). */
private boolean inLibrary;
/** Order count in search billing window for this product. */
private int orderCount;
}
}
@@ -0,0 +1,56 @@
package com.bms.price.domain.vo;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 订单比对异步任务进度(Redis)。
* ok=本地已有;fail/缺失=上游有而本地无(仅转单号)。
*/
public class TabOrderCompareTask implements Serializable
{
private static final long serialVersionUID = 1L;
public static final String STATUS_PENDING = "PENDING";
public static final String STATUS_RUNNING = "RUNNING";
public static final String STATUS_SUCCESS = "SUCCESS";
public static final String STATUS_FAIL = "FAIL";
private String taskId;
private String status;
private int percent;
private String message;
/** 已扫描上游转单号数 */
private int scanned;
/** 本地已存在 */
private int matched;
/** 本地缺失 */
private int missing;
private String error;
/** 缺失转单号样本(有上限) */
private List<String> missingTransferNos = new ArrayList<String>();
public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public int getPercent() { return percent; }
public void setPercent(int percent) { this.percent = percent; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public int getScanned() { return scanned; }
public void setScanned(int scanned) { this.scanned = scanned; }
public int getMatched() { return matched; }
public void setMatched(int matched) { this.matched = matched; }
public int getMissing() { return missing; }
public void setMissing(int missing) { this.missing = missing; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
public List<String> getMissingTransferNos() { return missingTransferNos; }
public void setMissingTransferNos(List<String> missingTransferNos)
{
this.missingTransferNos = missingTransferNos == null
? new ArrayList<String>() : missingTransferNos;
}
}
@@ -29,6 +29,15 @@ public interface OracleUpstreamOrderMapper
@Param("startRow") long startRow,
@Param("endRow") long endRow);
/**
* 订单比对专用(异步分页):仅返回 TRANSFER_ORDER_ID。
*/
List<Map<String, Object>> selectTabOrderCompareKeyPage(
@Param("orderTable") String orderTable,
@Param("query") TabOrderMonitorQuery query,
@Param("startRow") long startRow,
@Param("endRow") long endRow);
/** List detail by ids: display columns only. */
List<Map<String, Object>> selectTabOrderMonitorListByIds(
@Param("orderTable") String orderTable,
@@ -54,6 +63,16 @@ public interface OracleUpstreamOrderMapper
@Param("query") TabOrderMonitorQuery query,
@Param("batch") List<String> batch);
/**
* 比对回填转单号专用:仅按 TRANSFER_ORDER_ID IN 分批收集,
* 不走提货时间,与 selectMatchedOrderKeys 完全隔离。
*/
List<Map<String, Object>> selectMatchedTransferOrderKeys(
@Param("orderTable") String orderTable,
@Param("batch") List<String> batch,
@Param("customerCodes") List<String> customerCodes);
/** 按已收集到的订单主键批量查明细(一批为一个内存分页切片)。 */
List<Map<String, Object>> selectTabOrderMonitorByIds(
@Param("orderTable") String orderTable,
@@ -3,6 +3,7 @@ package com.bms.price.mapper;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PrcOrderBill;
@@ -20,6 +21,22 @@ public interface PrcOrderBillMapper
public List<PrcOrderBill> selectPrcOrderBillList(PrcOrderBill bill);
/** Distinct customer+product groups for product-check (order counts in filter window). */
List<Map<String, Object>> selectDistinctProductGroups(PrcOrderBill bill);
/** Drill-down orders for one product under product-check filters. */
List<PrcOrderBill> selectOrderBillsByProductForCheck(@Param("query") PrcOrderBill query, @Param("limit") int limit);
/**
* 订单比对:批量查本地已存在的单号(order_no / tracking_no / transfer_no)。
*/
List<String> selectExistingOrderIdentifiers(@Param("identifiers") List<String> identifiers,
@Param("customerCodes") List<String> customerCodes);
/** 订单比对:仅按转单号批量查本地已存在。 */
List<String> selectExistingTransferNos(@Param("transferNos") List<String> transferNos,
@Param("customerCodes") List<String> customerCodes);
/** 订单号下拉搜索(带数据权限) */
public List<String> selectOrderNoOptions(@Param("query") PrcOrderBill query, @Param("limit") int limit);
@@ -25,4 +25,11 @@ public class OracleOrderKeyCollectWorker
{
return oracleUpstreamOrderMapper.selectMatchedOrderKeys(table, query, batch);
}
/** 比对回填转单号专用分批(不走三列 UNION)。 */
@DataSource(DataSourceType.SLAVE)
public List<Map<String, Object>> collectTransferBatch(String table, List<String> batch, List<String> customerCodes)
{
return oracleUpstreamOrderMapper.selectMatchedTransferOrderKeys(table, batch, customerCodes);
}
}
@@ -62,10 +62,11 @@ public class TabOrderMonitorService
private static final int COLLECT_CACHE_MAX_ENTRIES = 20;
/**
* 客户+时间列表:一次拉取的主键上限。一次取齐后内存分页,总数稳定;
* 超过此数量仅展示前 N 条(避免 COUNT / 翻页估总数导致「共 101→201」)。
* 客户+时间列表:一次 ROWNUM 拉主键后内存分页(总数稳定)。
* 上限 8000:覆盖常见月窗口,又避免分批深翻 ROWNUM 把查询拖死。
* 全量差异请用「订单比对」异步任务。
*/
private static final int FILTER_LIST_KEY_LIMIT = 2000;
private static final int FILTER_LIST_KEY_LIMIT = 8000;
/** 单批 Oracle 查询最长等待(从库 socketTimeout 为 120s,留同样余量)。 */
private static final long COLLECT_BATCH_TIMEOUT_SECONDS = 120L;
@@ -121,6 +122,19 @@ public class TabOrderMonitorService
}
});
/** 客户+时间精确总数缓存(翻页复用,避免每页重复 COUNT)。 */
private final Map<String, FilterCountCacheEntry> filterCountCache =
Collections.synchronizedMap(new LinkedHashMap<String, FilterCountCacheEntry>(16, 0.75f, true)
{
private static final long serialVersionUID = 1L;
@Override
protected boolean removeEldestEntry(Map.Entry<String, FilterCountCacheEntry> eldest)
{
return size() > COLLECT_CACHE_MAX_ENTRIES;
}
});
private static final class CacheEntry
{
private final List<MatchedOrderKey> keys;
@@ -145,6 +159,18 @@ public class TabOrderMonitorService
}
}
private static final class FilterCountCacheEntry
{
private final long total;
private final long expireAt;
private FilterCountCacheEntry(long total, long expireAt)
{
this.total = total;
this.expireAt = expireAt;
}
}
@PreDestroy
public void shutdownCollectPool()
{
@@ -261,6 +287,72 @@ public class TabOrderMonitorService
}
}
/**
* 订单比对异步分页:专用 SQL 仅返回当前页转单号。
*/
@DataSource(DataSourceType.SLAVE)
public List<String> selectCompareTransferPage(TabOrderMonitorQuery query, int pageNum, int pageSize)
{
// 比对与回填列表完全隔离:先清单号再 validate,仅走客户+提货时间
if (query != null)
{
query.setOrderIdentifiers(null);
query.setOrderId(null);
query.setOrgOrderId(null);
query.setTransferOrderId(null);
}
validate(query);
List<String> codes = query.getCustomerCodes();
boolean hasCustomer = codes != null && !codes.isEmpty();
boolean hasTime = StringUtils.isNotEmpty(query.getBeginTime()) && StringUtils.isNotEmpty(query.getEndTime());
if (!hasCustomer || !hasTime)
{
throw new ServiceException("订单比对需先选择客户和提货时间");
}
int pn = pageNum < 1 ? 1 : pageNum;
int ps = pageSize < 1 ? 100 : pageSize;
if (ps > 1000)
{
ps = 1000;
}
long endRow = (long) pn * ps;
long startRow = endRow - ps;
String table = resolveTable();
try
{
PageHelper.clearPage();
List<Map<String, Object>> rows =
oracleUpstreamOrderMapper.selectTabOrderCompareKeyPage(table, query, startRow, endRow);
if (rows == null || rows.isEmpty())
{
return Collections.emptyList();
}
List<String> out = new ArrayList<String>(rows.size());
for (Map<String, Object> row : rows)
{
Object v = value(row, "TRANSFER_ORDER_ID");
if (v == null)
{
continue;
}
String tn = String.valueOf(v).trim();
if (!tn.isEmpty())
{
out.add(tn);
}
}
return out;
}
catch (RuntimeException e)
{
throw new ServiceException("Oracle TAB_ORDER compare page failed: " + e.getMessage());
}
finally
{
PageHelper.clearPage();
}
}
/**
* 列表查询统一入口:一次数据源调用内返回总数 + 当前页,
* 大批量单号场景只做一次分批收集,避免 count 和 page 重复扫描。
@@ -279,15 +371,67 @@ public class TabOrderMonitorService
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(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);
}
// Customer+time: collect keys once (capped), memory-page; stable total (no 101->201).
// Customer+time: exact COUNT(1) + current-page keys.
return doSelectFilterListPage(table, query, pn, ps);
}
/**
* 比对缺失转单号回填搜索专用入口:
* 仅按 TRANSFER_ORDER_ID 分批收集,不带提货时间,
* 与列表/同步/比对共用 searchPage 完全隔离。
*/
@DataSource(DataSourceType.SLAVE)
public TabOrderPageResult searchPageByTransferNos(TabOrderMonitorQuery query, int pageNum, int pageSize)
{
if (query == null || query.getOrderIdentifiers() == null || query.getOrderIdentifiers().isEmpty())
{
throw new ServiceException("转单号不能为空");
}
List<String> nos = normalizeLineList(query.getOrderIdentifiers());
if (nos == null || nos.isEmpty())
{
throw new ServiceException("转单号不能为空");
}
if (nos.size() > IDENTIFIER_HARD_LIMIT)
{
throw new ServiceException("单号数量超过服务端上限 " + IDENTIFIER_HARD_LIMIT
+ ",请分批查询,避免内存溢出");
}
// optional customer filter only; never use pickup time on this path
List<String> codes = query.getCustomerCodes();
List<String> cleanedCodes = new ArrayList<String>();
if (codes != null)
{
for (String c : codes)
{
if (StringUtils.isNotEmpty(c))
{
cleanedCodes.add(c.trim());
}
}
}
List<String> customerCodes = cleanedCodes.isEmpty() ? null : cleanedCodes;
int[] page = normalizePage(pageNum, pageSize);
int pn = page[0];
int ps = page[1];
String table = resolveTable();
long t0 = System.currentTimeMillis();
List<MatchedOrderKey> keys = doCollectTransferKeys(table, nos, customerCodes);
List<TabOrderMonitorVo> rows = doPageByKeys(table, keys, pn, ps);
log.info("oracle tab_order transfer-fill page transferNos={} matched={} page={}/{} rows={} costMs={}",
Integer.valueOf(nos.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);
}
@DataSource(DataSourceType.SLAVE)
public List<TabOrderMonitorVo> selectPage(TabOrderMonitorQuery query, int pageNum, int pageSize)
{
@@ -360,34 +504,67 @@ public class TabOrderMonitorService
}
/**
* Customer+time list: pull up to {@link #FILTER_LIST_KEY_LIMIT} keys once (cached),
* then memory-page and load display rows by ID. Total stays stable across pages.
* Customer+time list: exact COUNT(1) for total + key-page for current rows.
* Total uses countTabOrderMonitor (same filters as Oct-13 style COUNT).
*/
private TabOrderPageResult doSelectFilterListPage(String table, TabOrderMonitorQuery query, int pn, int ps)
{
long t0 = System.currentTimeMillis();
List<String> allIds = doCollectFilterKeyIds(table, query);
int total = allIds.size();
int from = (pn - 1) * ps;
if (from >= total)
long total = countFilterTotal(table, query);
long startRow = (long) (pn - 1) * ps;
long endRow = startRow + ps;
if (startRow >= total)
{
log.info("oracle tab_order filter-list customerSize={} begin={} end={} page={}/{} rows=0 total={} costMs={}",
query.getCustomerCodes() == null ? 0 : query.getCustomerCodes().size(),
query.getBeginTime(), query.getEndTime(), pn, ps, Integer.valueOf(total),
query.getBeginTime(), query.getEndTime(), pn, ps, Long.valueOf(total),
Long.valueOf(System.currentTimeMillis() - t0));
return new TabOrderPageResult(total, Collections.emptyList());
}
List<String> slice = allIds.subList(from, Math.min(from + ps, total));
List<TabOrderMonitorVo> list = loadListVosByIds(table, new ArrayList<String>(slice));
List<String> ids = selectKeyIds(table, query, startRow, endRow);
List<TabOrderMonitorVo> list = loadListVosByIds(table, ids);
log.info("oracle tab_order filter-list customerSize={} begin={} end={} page={}/{} rows={} total={} costMs={}",
query.getCustomerCodes() == null ? 0 : query.getCustomerCodes().size(),
query.getBeginTime(), query.getEndTime(), pn, ps,
Integer.valueOf(list.size()), Integer.valueOf(total),
Integer.valueOf(list.size()), Long.valueOf(total),
Long.valueOf(System.currentTimeMillis() - t0));
return new TabOrderPageResult(total, list);
}
/** Collect (and cache) filter-path primary keys, newest first, capped at FILTER_LIST_KEY_LIMIT. */
/** Exact total for customer+time list; short TTL cache to avoid recount on every page flip. */
private long countFilterTotal(String table, TabOrderMonitorQuery query)
{
String fingerprint = "count|" + collectFingerprint(table, query);
FilterCountCacheEntry cached = filterCountCache.get(fingerprint);
long now = System.currentTimeMillis();
if (cached != null && cached.expireAt > now)
{
return cached.total;
}
try
{
PageHelper.clearPage();
long t0 = System.currentTimeMillis();
long total = oracleUpstreamOrderMapper.countTabOrderMonitor(table, query);
log.info("oracle tab_order count customerSize={} begin={} end={} total={} costMs={}",
query.getCustomerCodes() == null ? 0 : query.getCustomerCodes().size(),
query.getBeginTime(), query.getEndTime(),
Long.valueOf(total), Long.valueOf(System.currentTimeMillis() - t0));
filterCountCache.put(fingerprint,
new FilterCountCacheEntry(total, now + COLLECT_CACHE_TTL_MS));
return total;
}
catch (RuntimeException e)
{
throw new ServiceException("Oracle TAB_ORDER count failed: " + e.getMessage());
}
finally
{
PageHelper.clearPage();
}
}
/** Collect (and cache) filter-path primary keys, newest first; hard-capped at FILTER_LIST_KEY_LIMIT. */
private List<String> doCollectFilterKeyIds(String table, TabOrderMonitorQuery query)
{
String fingerprint = "filter|" + collectFingerprint(table, query);
@@ -397,11 +574,11 @@ public class TabOrderMonitorService
{
return new ArrayList<String>(cached.ids);
}
// Fetch limit+1 to detect cap; keep at most FILTER_LIST_KEY_LIMIT.
// One Oracle key-page (ROWNUM <= limit+1). Avoid deep multi-page ROWNUM which hangs month windows.
List<String> ids = selectKeyIds(table, query, 0L, FILTER_LIST_KEY_LIMIT + 1L);
if (ids.size() > FILTER_LIST_KEY_LIMIT)
{
log.warn("oracle tab_order filter-list capped at {} keys (customer+time window too large); narrow pickup range",
log.warn("oracle tab_order filter-list capped at {} keys (customer+time window too large); use order compare for full diff",
Integer.valueOf(FILTER_LIST_KEY_LIMIT));
ids = new ArrayList<String>(ids.subList(0, FILTER_LIST_KEY_LIMIT));
}
@@ -575,6 +752,118 @@ public class TabOrderMonitorService
* 查 Oracle(每批 UNION ALL 三列各走索引),Java 侧按 ID 去重,
* 再按 PICKUP_DATE DESC, ID DESC 排序。相同查询条件短期缓存,翻页/重复查询秒回。
*/
/**
* 回填转单号专用收集:只走 selectMatchedTransferOrderKeys,
* 不走三列 UNION、不带提货时间。
*/
private List<MatchedOrderKey> doCollectTransferKeys(String table, List<String> transferNos, List<String> customerCodes)
{
StringBuilder fp = new StringBuilder("tf|").append(table == null ? "" : table).append('|');
appendSorted(fp, transferNos);
fp.append('|');
appendSorted(fp, customerCodes);
String fingerprint = Integer.toHexString(fp.toString().hashCode());
long now = System.currentTimeMillis();
CacheEntry cached = collectCache.get(fingerprint);
if (cached != null && now < cached.expireAt)
{
log.info("oracle tab_order transfer-fill cache-hit transferNos={} matched={}",
Integer.valueOf(transferNos.size()), Integer.valueOf(cached.keys.size()));
return new ArrayList<MatchedOrderKey>(cached.keys);
}
List<List<String>> batches = new ArrayList<List<String>>();
for (int bi = 0; bi < transferNos.size(); bi += IDENTIFIER_BATCH_SIZE)
{
batches.add(transferNos.subList(bi, Math.min(bi + IDENTIFIER_BATCH_SIZE, transferNos.size())));
}
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.collectTransferBatch(table, batch, customerCodes)));
}
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 transfer-fill collect failed: " + cause.getMessage());
}
catch (TimeoutException e)
{
future.cancel(true);
throw new ServiceException("Oracle transfer-fill collect timeout");
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
throw new ServiceException("Oracle transfer-fill 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;
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()));
}
keys.sort(Comparator
.comparing(MatchedOrderKey::getPickupDate,
Comparator.nullsFirst(Comparator.<Timestamp>reverseOrder()))
.thenComparing(MatchedOrderKey::getId, Comparator.<String>reverseOrder()));
log.info("oracle tab_order transfer-fill collect transferNos={} batches={} matched={} costMs={}",
Integer.valueOf(transferNos.size()), Integer.valueOf(batches.size()),
Integer.valueOf(keys.size()), Long.valueOf(System.currentTimeMillis() - t0));
collectCache.put(fingerprint, new CacheEntry(Collections.unmodifiableList(keys),
System.currentTimeMillis() + COLLECT_CACHE_TTL_MS));
return keys;
}
private List<MatchedOrderKey> doCollectMatchedKeys(TabOrderMonitorQuery query, String table)
{
List<String> identifiers = query.getOrderIdentifiers();
@@ -0,0 +1,317 @@
package com.bms.price.service;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.bms.common.annotation.DataScope;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.StringUtils;
import com.bms.price.domain.BmsProduct;
import com.bms.price.domain.PrcOrderBill;
import com.bms.price.domain.PrcPriceSetProduct;
import com.bms.price.domain.vo.OrderBillProductCheckResult;
import com.bms.price.domain.vo.OrderBillProductCheckResult.ProductRow;
import com.bms.price.mapper.BmsProductMapper;
import com.bms.price.mapper.PrcOrderBillMapper;
/**
* Detect package sales products (as of billing end time) that are missing from product catalog.
*/
@Service
public class OrderBillProductCheckService
{
private static final int ORDER_DRILL_LIMIT = 500;
@Autowired
private ITmsPriceQuoteService tmsPriceQuoteService;
@Autowired
private BmsProductMapper productMapper;
@Autowired
private PrcOrderBillMapper orderBillMapper;
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id",
permission = "price:orderBill:list")
public OrderBillProductCheckResult check(PrcOrderBill query)
{
validate(query);
Date asOf = resolveAsOf(query);
OrderBillProductCheckResult result = new OrderBillProductCheckResult();
result.setAsOfTime(formatAsOf(asOf));
List<String> customerCodes = normalizeCodes(query.getCustomerCodes());
Map<String, Integer> orderCountByKey = loadOrderCounts(query);
long orderTotal = 0L;
for (Integer c : orderCountByKey.values())
{
if (c != null)
{
orderTotal += c.intValue();
}
}
result.setOrderTotal(orderTotal);
List<ProductRow> packageRows = new ArrayList<ProductRow>();
List<ProductRow> missingRows = new ArrayList<ProductRow>();
int matched = 0;
for (String customerCode : customerCodes)
{
List<PrcPriceSetProduct> products = tmsPriceQuoteService.listAllBoundPackageProducts(customerCode, asOf);
Set<String> seen = new HashSet<String>();
if (products == null)
{
continue;
}
for (PrcPriceSetProduct p : products)
{
if (p == null || StringUtils.isEmpty(p.getProductCode()))
{
continue;
}
String code = p.getProductCode().trim();
String upper = code.toUpperCase(Locale.ROOT);
if (!seen.add(upper))
{
continue;
}
boolean inLibrary = existsInLibrary(code, asOf);
ProductRow row = new ProductRow();
row.setCustomerCode(customerCode);
row.setProductCode(code);
row.setProductName(trim(p.getProductName()));
row.setInLibrary(inLibrary);
Integer oc = orderCountByKey.get(key(customerCode, upper));
row.setOrderCount(oc == null ? 0 : oc.intValue());
packageRows.add(row);
if (inLibrary)
{
matched++;
}
else
{
missingRows.add(row);
}
}
}
packageRows.sort(Comparator
.comparing(ProductRow::getCustomerCode, nullSafe())
.thenComparing(ProductRow::getProductCode, nullSafe()));
missingRows.sort(Comparator
.comparing(ProductRow::getCustomerCode, nullSafe())
.thenComparing(ProductRow::getProductCode, nullSafe()));
result.setPackageProductCount(packageRows.size());
result.setMatchedCount(matched);
result.setMissingCount(missingRows.size());
result.setPackageProducts(packageRows);
result.setMissingProducts(missingRows);
return result;
}
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id",
permission = "price:orderBill:list")
public List<PrcOrderBill> listOrdersByProduct(PrcOrderBill query)
{
validate(query);
if (StringUtils.isEmpty(query.getProductCode()))
{
throw new ServiceException("productCode required");
}
query.setProductCode(query.getProductCode().trim());
List<PrcOrderBill> rows = orderBillMapper.selectOrderBillsByProductForCheck(query, ORDER_DRILL_LIMIT);
return rows == null ? new ArrayList<PrcOrderBill>() : rows;
}
private Map<String, Integer> loadOrderCounts(PrcOrderBill query)
{
List<Map<String, Object>> groups = orderBillMapper.selectDistinctProductGroups(query);
Map<String, Integer> map = new HashMap<String, Integer>();
if (groups == null)
{
return map;
}
for (Map<String, Object> g : groups)
{
String customerCode = trim(str(g, "customer_code"));
String productCode = trim(str(g, "product_code"));
if (StringUtils.isEmpty(customerCode) || StringUtils.isEmpty(productCode))
{
continue;
}
map.put(key(customerCode, productCode.toUpperCase(Locale.ROOT)),
Integer.valueOf((int) Math.min(Integer.MAX_VALUE, longValue(g, "order_count"))));
}
return map;
}
private boolean existsInLibrary(String productCode, Date asOf)
{
BmsProduct hit = productMapper.selectEffectiveBmsProductByCode(productCode, asOf);
return hit != null && StringUtils.isNotEmpty(hit.getProductCode());
}
private void validate(PrcOrderBill query)
{
if (query == null)
{
throw new ServiceException("customer and billing time required");
}
List<String> codes = normalizeCodes(query.getCustomerCodes());
if (codes.isEmpty())
{
throw new ServiceException("please select customer");
}
query.setCustomerCodes(codes);
Map<String, Object> params = query.getParams();
String begin = params == null ? null : strObj(params.get("beginBizDate"));
String end = params == null ? null : strObj(params.get("endBizDate"));
if (StringUtils.isEmpty(begin) || StringUtils.isEmpty(end))
{
throw new ServiceException("please select billing time range");
}
}
private static List<String> normalizeCodes(List<String> codes)
{
if (codes == null || codes.isEmpty())
{
return new ArrayList<String>();
}
LinkedHashSet<String> set = new LinkedHashSet<String>();
for (String c : codes)
{
if (StringUtils.isNotEmpty(c))
{
set.add(c.trim());
}
}
return new ArrayList<String>(set);
}
private static Date resolveAsOf(PrcOrderBill query)
{
Map<String, Object> params = query.getParams();
String raw = params == null ? null : strObj(params.get("endBizDate"));
if (StringUtils.isEmpty(raw) && params != null)
{
raw = strObj(params.get("beginBizDate"));
}
Date parsed = parseDateTime(raw);
return parsed != null ? parsed : new Date();
}
private static Date parseDateTime(String raw)
{
if (StringUtils.isEmpty(raw))
{
return null;
}
String s = raw.trim().replace('T', ' ');
try
{
if (s.length() >= 19)
{
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(s.substring(0, 19));
}
if (s.length() >= 10)
{
return new SimpleDateFormat("yyyy-MM-dd").parse(s.substring(0, 10));
}
}
catch (ParseException ignore)
{
return null;
}
return null;
}
private static String formatAsOf(Date asOf)
{
if (asOf == null)
{
return null;
}
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(asOf);
}
private static String key(String customerCode, String productUpper)
{
return customerCode + "|" + productUpper;
}
private static String trim(String s)
{
return StringUtils.isEmpty(s) ? null : s.trim();
}
private static String strObj(Object v)
{
return v == null ? null : String.valueOf(v).trim();
}
private static Comparator<String> nullSafe()
{
return Comparator.nullsFirst(String.CASE_INSENSITIVE_ORDER);
}
private static String str(Map<String, Object> row, String key)
{
Object v = value(row, key);
return v == null ? null : String.valueOf(v).trim();
}
private static long longValue(Map<String, Object> row, String key)
{
Object v = value(row, key);
if (v == null)
{
return 0L;
}
if (v instanceof Number)
{
return ((Number) v).longValue();
}
try
{
return Long.parseLong(String.valueOf(v).trim());
}
catch (Exception e)
{
return 0L;
}
}
private static Object value(Map<String, Object> row, String key)
{
if (row == null || key == null)
{
return null;
}
if (row.containsKey(key))
{
return row.get(key);
}
for (Map.Entry<String, Object> e : row.entrySet())
{
if (e.getKey() != null && e.getKey().equalsIgnoreCase(key))
{
return e.getValue();
}
}
return null;
}
}
@@ -0,0 +1,211 @@
package com.bms.price.task;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.bms.common.core.redis.RedisCache;
import com.bms.common.utils.StringUtils;
import com.bms.price.domain.vo.TabOrderCompareTask;
import com.bms.price.domain.vo.TabOrderMonitorQuery;
import com.bms.price.mapper.PrcOrderBillMapper;
import com.bms.price.oracle.TabOrderMonitorService;
import com.bms.price.search.IBillSearchStore;
import com.bms.price.search.TabOrderSyncEsLog;
/**
* 订单比对异步任务:分页拉上游转单号,与本地 transfer_no 比对差异。
*/
@Component
public class TabOrderCompareAsyncRunner
{
private static final Logger log = LoggerFactory.getLogger(TabOrderCompareAsyncRunner.class);
public static final String CACHE_KEY_PREFIX = "billing:orderSource:compare:";
public static final int TTL_MINUTES = 120;
public static final int PAGE_SIZE = 500;
/** 单次任务最多扫描上游转单号数(防崩) */
public static final int SCAN_HARD_LIMIT = 50000;
/** 进度/抽屉中保留的缺失转单号上限 */
public static final int MISSING_SAMPLE_LIMIT = 5000;
@Autowired
private TabOrderMonitorService tabOrderMonitorService;
@Autowired
private PrcOrderBillMapper prcOrderBillMapper;
@Autowired
private RedisCache redisCache;
@Autowired
private IBillSearchStore billSearchStore;
@Async("threadPoolTaskExecutor")
public void run(String asyncTaskId, TabOrderMonitorQuery query)
{
try
{
if (query != null)
{
query.setOrderIdentifiers(null);
query.setOrderId(null);
query.setOrgOrderId(null);
query.setTransferOrderId(null);
}
put(asyncTaskId, TabOrderCompareTask.STATUS_RUNNING, 0, "starting", 0, 0, 0, null, null);
int scanned = 0;
int matched = 0;
int missing = 0;
List<String> missingSample = new ArrayList<String>();
int pageNum = 1;
while (true)
{
if (scanned >= SCAN_HARD_LIMIT)
{
log.warn("orderSource compare capped at {} asyncTaskId={}", Integer.valueOf(SCAN_HARD_LIMIT), asyncTaskId);
break;
}
int remain = SCAN_HARD_LIMIT - scanned;
int pageSize = Math.min(PAGE_SIZE, remain);
List<String> transferNos = tabOrderMonitorService.selectCompareTransferPage(query, pageNum, pageSize);
if (transferNos == null || transferNos.isEmpty())
{
break;
}
Set<String> existing = findExisting(transferNos, query == null ? null : query.getCustomerCodes());
for (String tn : transferNos)
{
if (StringUtils.isEmpty(tn))
{
continue;
}
scanned++;
String key = tn.trim().toUpperCase(Locale.ROOT);
if (existing.contains(key))
{
matched++;
}
else
{
missing++;
if (missingSample.size() < MISSING_SAMPLE_LIMIT)
{
missingSample.add(tn.trim());
}
}
}
int percent = Math.min(99, (int) ((scanned * 100L) / Math.max(scanned + pageSize, 1)));
// 最后一页不满页时近乎完成;否则按已扫描量给一个缓慢增长的百分比
if (transferNos.size() < pageSize)
{
percent = 99;
}
put(asyncTaskId, TabOrderCompareTask.STATUS_RUNNING, percent,
"scanning page=" + pageNum + " scanned=" + scanned + " missing=" + missing,
scanned, matched, missing, null, missingSample);
if (transferNos.size() < pageSize)
{
break;
}
pageNum++;
}
Map<String, Object> resp = new LinkedHashMap<String, Object>();
resp.put("asyncTaskId", asyncTaskId);
resp.put("scanned", Integer.valueOf(scanned));
resp.put("matched", Integer.valueOf(matched));
resp.put("missing", Integer.valueOf(missing));
resp.put("missingTransferNos", missingSample);
resp.put("missingTruncated", Boolean.valueOf(missing > missingSample.size()));
put(asyncTaskId, TabOrderCompareTask.STATUS_SUCCESS, 100,
"done scanned=" + scanned + " missing=" + missing,
scanned, matched, missing, null, missingSample);
TabOrderSyncEsLog.save(billSearchStore, asyncTaskId, "COMPARE_DONE", query, resp, "SUCCESS", null);
log.info("orderSource compare done asyncTaskId={} scanned={} matched={} missing={}",
asyncTaskId, Integer.valueOf(scanned), Integer.valueOf(matched), Integer.valueOf(missing));
}
catch (Exception e)
{
log.error("orderSource compare failed asyncTaskId={}", asyncTaskId, e);
String msg = e.getMessage();
if (StringUtils.isEmpty(msg))
{
msg = e.getClass().getSimpleName();
}
put(asyncTaskId, TabOrderCompareTask.STATUS_FAIL, 100, "failed", 0, 0, 0, msg, null);
TabOrderSyncEsLog.save(billSearchStore, asyncTaskId, "COMPARE_DONE", query, null, "FAIL", msg);
}
}
private Set<String> findExisting(List<String> transferNos, List<String> customerCodes)
{
Set<String> found = new HashSet<String>();
if (transferNos == null || transferNos.isEmpty())
{
return found;
}
List<String> hit = prcOrderBillMapper.selectExistingTransferNos(transferNos, cleanCodes(customerCodes));
if (hit == null)
{
return found;
}
for (String s : hit)
{
if (StringUtils.isNotEmpty(s))
{
found.add(s.trim().toUpperCase(Locale.ROOT));
}
}
return found;
}
private static List<String> cleanCodes(List<String> codes)
{
if (codes == null || codes.isEmpty())
{
return null;
}
List<String> out = new ArrayList<String>();
for (String c : codes)
{
if (StringUtils.isNotEmpty(c))
{
out.add(c.trim());
}
}
return out.isEmpty() ? null : out;
}
public void put(String taskId, String status, int percent, String message,
int scanned, int matched, int missing, String error, List<String> missingSample)
{
TabOrderCompareTask task = new TabOrderCompareTask();
task.setTaskId(taskId);
task.setStatus(status);
task.setPercent(percent);
task.setMessage(message);
task.setScanned(scanned);
task.setMatched(matched);
task.setMissing(missing);
task.setError(error);
if (missingSample != null)
{
task.setMissingTransferNos(new ArrayList<String>(missingSample));
}
redisCache.setCacheObject(CACHE_KEY_PREFIX + taskId, task, TTL_MINUTES, TimeUnit.MINUTES);
}
public TabOrderCompareTask get(String taskId)
{
return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
}
}
@@ -154,6 +154,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
WHERE rn &gt; #{startRow}
</select>
<!--
订单比对专用 SQL(异步分页):仅返回转单号,
筛选与列表相同;排除空转单号。
-->
<select id="selectTabOrderCompareKeyPage" resultType="java.util.HashMap">
SELECT TRANSFER_ORDER_ID FROM (
SELECT inner_q.TRANSFER_ORDER_ID AS TRANSFER_ORDER_ID, ROWNUM AS rn
FROM (
SELECT o.TRANSFER_ORDER_ID, o.PICKUP_DATE, o.ORDER_ID
FROM ${orderTable} o
WHERE
<include refid="tabOrderMonitorWhere"/>
AND o.TRANSFER_ORDER_ID IS NOT NULL
ORDER BY o.PICKUP_DATE DESC, o.ORDER_ID DESC
) inner_q
WHERE ROWNUM &lt;= #{endRow}
)
WHERE rn &gt; #{startRow}
</select>
<!-- List detail by primary keys (display columns only). -->
<select id="selectTabOrderMonitorListByIds" resultType="java.util.HashMap">
SELECT
@@ -195,10 +215,10 @@ 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 侧去重。
<!--
大批量单号分批收集:每批最多 900 个。
三列 UNION ALL(订单号/运单号/转单号),叠加 tabOrderFilterWhere。
比对回填搜索请用 selectMatchedTransferOrderKeys,不走本 SQL。
-->
<select id="selectMatchedOrderKeys" resultType="java.util.HashMap">
SELECT * FROM (
@@ -231,6 +251,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
)
</select>
<!--
比对缺失转单号回填搜索专用:仅 TRANSFER_ORDER_ID IN,
不带提货时间,不影响列表/同步/比对共用 SQL。
-->
<select id="selectMatchedTransferOrderKeys" resultType="java.util.HashMap">
SELECT o.ID AS ID, o.PICKUP_DATE AS PICKUP_DATE
FROM ${orderTable} o
WHERE o.TRANSFER_ORDER_ID IN
<foreach collection="batch" item="bid" open="(" separator="," close=")">
#{bid}
</foreach>
AND o.BL_PICKUP = 1
AND o.ORDER_STATUS IN (3, 4, 5, 6, 7, 8, 11)
<if test="customerCodes != null and customerCodes.size() > 0">
AND o.CUSTOMER_CODE IN
<foreach collection="customerCodes" item="code" open="(" separator="," close=")">
#{code}
</foreach>
</if>
</select>
<!-- 内存分页切片后的主键回查(一批最多一个页大小,远低于 IN 上限) -->
<select id="selectTabOrderMonitorByIds" resultType="java.util.HashMap">
<!-- 全行数据:同步计费时直接复用,不再按单号回查 Oracle -->
@@ -204,6 +204,69 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</if>
</sql>
<!-- 订单比对:仅转单号 -->
<select id="selectExistingTransferNos" resultType="String">
SELECT DISTINCT p.transfer_no
FROM prc_order_bill p
WHERE p.transfer_no IN
<foreach collection="transferNos" item="tn" open="(" separator="," close=")">
#{tn}
</foreach>
<if test="customerCodes != null and customerCodes.size() > 0">
AND p.customer_code IN
<foreach collection="customerCodes" item="ccode" open="(" separator="," close=")">
#{ccode}
</foreach>
</if>
AND p.transfer_no IS NOT NULL AND p.transfer_no != ''
</select>
<!-- 订单比对:上游单号中已落库本地的 order_no/tracking_no/transfer_no -->
<select id="selectExistingOrderIdentifiers" resultType="String">
SELECT DISTINCT hit.idn
FROM (
SELECT p.order_no AS idn
FROM prc_order_bill p
WHERE p.order_no IN
<foreach collection="identifiers" item="idn" open="(" separator="," close=")">
#{idn}
</foreach>
<if test="customerCodes != null and customerCodes.size() > 0">
AND p.customer_code IN
<foreach collection="customerCodes" item="ccode" open="(" separator="," close=")">
#{ccode}
</foreach>
</if>
UNION
SELECT p.tracking_no AS idn
FROM prc_order_bill p
WHERE p.tracking_no IN
<foreach collection="identifiers" item="idn" open="(" separator="," close=")">
#{idn}
</foreach>
<if test="customerCodes != null and customerCodes.size() > 0">
AND p.customer_code IN
<foreach collection="customerCodes" item="ccode" open="(" separator="," close=")">
#{ccode}
</foreach>
</if>
UNION
SELECT p.transfer_no AS idn
FROM prc_order_bill p
WHERE p.transfer_no IN
<foreach collection="identifiers" item="idn" open="(" separator="," close=")">
#{idn}
</foreach>
<if test="customerCodes != null and customerCodes.size() > 0">
AND p.customer_code IN
<foreach collection="customerCodes" item="ccode" open="(" separator="," close=")">
#{ccode}
</foreach>
</if>
) hit
WHERE hit.idn IS NOT NULL AND hit.idn != ''
</select>
<select id="selectOrderNoOptions" resultType="String">
select p.order_no
from prc_order_bill p
@@ -246,6 +309,106 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
limit #{limit}
</select>
<!-- Product-check filters: customer + billing time (+ optional status/identifiers). -->
<sql id="orderBillProductCheckWhere">
<if test="customerCodes != null and customerCodes.size() > 0">
AND p.customer_code IN
<foreach collection="customerCodes" item="ccode" open="(" separator="," close=")">
#{ccode}
</foreach>
</if>
<if test="params != null and params.beginBizDate != null and params.beginBizDate != ''">
AND p.biz_date &gt;= #{params.beginBizDate}
</if>
<if test="params != null and params.endBizDate != null and params.endBizDate != ''">
AND p.biz_date &lt;= #{params.endBizDate}
</if>
<if test="params != null and params.beginPickupDate != null and params.beginPickupDate != ''">
AND p.pickup_date &gt;= #{params.beginPickupDate}
</if>
<if test="params != null and params.endPickupDate != null and params.endPickupDate != ''">
AND p.pickup_date &lt;= #{params.endPickupDate}
</if>
<if test="orderStatuses != null and orderStatuses.size() > 0">
AND p.order_status IN
<foreach collection="orderStatuses" item="os" open="(" separator="," close=")">
#{os}
</foreach>
</if>
<if test="billingFailStatuses != null and billingFailStatuses.size() > 0">
AND p.billing_fail_status IN
<foreach collection="billingFailStatuses" item="bfs" open="(" separator="," close=")">
#{bfs}
</foreach>
</if>
<if test="orderIdentifiers != null and orderIdentifiers.size() > 0">
AND (
p.order_no IN
<foreach collection="orderIdentifiers" item="idn" open="(" separator="," close=")">
#{idn}
</foreach>
OR p.tracking_no IN
<foreach collection="orderIdentifiers" item="idn" open="(" separator="," close=")">
#{idn}
</foreach>
OR p.transfer_no IN
<foreach collection="orderIdentifiers" item="idn" open="(" separator="," close=")">
#{idn}
</foreach>
)
</if>
<include refid="orderBillDataScopeFilter"/>
</sql>
<select id="selectDistinctProductGroups" parameterType="PrcOrderBill" resultType="java.util.HashMap">
SELECT p.customer_code AS customer_code,
p.product_code AS product_code,
MAX(p.product_name) AS product_name,
COUNT(1) AS order_count
FROM prc_order_bill p
WHERE p.product_code IS NOT NULL
AND p.product_code &lt;&gt; ''
<include refid="orderBillProductCheckWhere"/>
GROUP BY p.customer_code, p.product_code
ORDER BY p.customer_code, p.product_code
</select>
<select id="selectOrderBillsByProductForCheck" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
<where>
<if test="query.productCode != null and query.productCode != ''">
AND p.product_code = #{query.productCode}
</if>
<if test="query.customerCodes != null and query.customerCodes.size() > 0">
AND p.customer_code IN
<foreach collection="query.customerCodes" item="ccode" open="(" separator="," close=")">
#{ccode}
</foreach>
</if>
<if test="query.params != null and query.params.beginBizDate != null and query.params.beginBizDate != ''">
AND p.biz_date &gt;= #{query.params.beginBizDate}
</if>
<if test="query.params != null and query.params.endBizDate != null and query.params.endBizDate != ''">
AND p.biz_date &lt;= #{query.params.endBizDate}
</if>
<if test="query.params != null and query.params.beginPickupDate != null and query.params.beginPickupDate != ''">
AND p.pickup_date &gt;= #{query.params.beginPickupDate}
</if>
<if test="query.params != null and query.params.endPickupDate != null and query.params.endPickupDate != ''">
AND p.pickup_date &lt;= #{query.params.endPickupDate}
</if>
<if test="query.orderStatuses != null and query.orderStatuses.size() > 0">
AND p.order_status IN
<foreach collection="query.orderStatuses" item="os" open="(" separator="," close=")">
#{os}
</foreach>
</if>
<include refid="orderBillDataScopeFilterByQuery"/>
</where>
order by p.biz_date desc, p.bill_id desc
limit #{limit}
</select>
<select id="selectPrcOrderBillList" parameterType="PrcOrderBill" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
<where>
@@ -364,6 +527,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="params.endBizDate != null and params.endBizDate != ''">
AND p.biz_date &lt;= #{params.endBizDate}
</if>
<if test="params.beginPickupDate != null and params.beginPickupDate != ''">
AND p.pickup_date &gt;= #{params.beginPickupDate}
</if>
<if test="params.endPickupDate != null and params.endPickupDate != ''">
AND p.pickup_date &lt;= #{params.endPickupDate}
</if>
<if test="params.beginTime != null and params.beginTime != ''">
AND date_format(p.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
</if>