订单数据源增加订单对比功能
This commit is contained in:
+75
@@ -24,6 +24,7 @@ import com.bms.common.core.page.TableSupport;
|
||||
import com.bms.common.enums.BusinessType;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.price.domain.PrcBillingTaskRetryTask;
|
||||
import com.bms.price.domain.vo.TabOrderCompareTask;
|
||||
import com.bms.price.domain.vo.TabOrderMonitorQuery;
|
||||
import com.bms.price.domain.vo.TabOrderMonitorVo;
|
||||
import com.bms.price.domain.vo.TabOrderProductCheckResult;
|
||||
@@ -35,6 +36,7 @@ import com.bms.price.search.BillSearchPage;
|
||||
import com.bms.price.search.EsLogType;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.search.TabOrderSyncEsLog;
|
||||
import com.bms.price.task.TabOrderCompareAsyncRunner;
|
||||
import com.bms.price.task.TabOrderSyncAsyncRunner;
|
||||
|
||||
/**
|
||||
@@ -53,6 +55,9 @@ public class TabOrderMonitorController extends BaseController
|
||||
@Autowired
|
||||
private TabOrderSyncAsyncRunner tabOrderSyncAsyncRunner;
|
||||
|
||||
@Autowired
|
||||
private TabOrderCompareAsyncRunner tabOrderCompareAsyncRunner;
|
||||
|
||||
@Autowired
|
||||
private IBillSearchStore billSearchStore;
|
||||
|
||||
@@ -82,6 +87,36 @@ public class TabOrderMonitorController extends BaseController
|
||||
pageSize == null ? 10 : pageSize.intValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* 比对缺失转单号回填搜索专用:仅按转单号列查询,
|
||||
* 不走普通列表/三列单号收集路径。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:list')")
|
||||
@PostMapping("/listByTransferNos")
|
||||
public TableDataInfo listByTransferNos(@RequestBody TabOrderMonitorQuery query,
|
||||
@RequestParam(value = "pageNum", required = false) Integer pageNum,
|
||||
@RequestParam(value = "pageSize", required = false) Integer pageSize)
|
||||
{
|
||||
TableDataInfo rsp = new TableDataInfo();
|
||||
rsp.setCode(HttpStatus.SUCCESS);
|
||||
rsp.setMsg("查询成功");
|
||||
if (query == null || !hasNonBlankItem(query.getOrderIdentifiers()))
|
||||
{
|
||||
rsp.setRows(Collections.emptyList());
|
||||
rsp.setTotal(0L);
|
||||
return rsp;
|
||||
}
|
||||
// clear time so shared resolvers cannot accidentally reuse month window
|
||||
query.setBeginTime(null);
|
||||
query.setEndTime(null);
|
||||
customerKeywordResolver.resolve(query);
|
||||
TabOrderPageResult page = tabOrderMonitorService.searchPageByTransferNos(
|
||||
query, pageNum == null ? 1 : pageNum.intValue(), pageSize == null ? 10 : pageSize.intValue());
|
||||
rsp.setRows(page.getRows());
|
||||
rsp.setTotal(page.getTotal());
|
||||
return rsp;
|
||||
}
|
||||
|
||||
private TableDataInfo listPage(TabOrderMonitorQuery query, int pageNum, int pageSize)
|
||||
{
|
||||
TableDataInfo rsp = new TableDataInfo();
|
||||
@@ -102,6 +137,46 @@ public class TabOrderMonitorController extends BaseController
|
||||
return rsp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单比对(异步):按客户+提货时间分页比对转单号(上游有/本地无)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:list')")
|
||||
@Log(title = "order source compare", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/compareByFilter")
|
||||
public AjaxResult compareByFilter(@RequestBody TabOrderMonitorQuery query)
|
||||
{
|
||||
if (!hasRequiredFilters(query))
|
||||
{
|
||||
return error("订单比对需先选择客户和提货时间");
|
||||
}
|
||||
// 比对不受列表单号/回填影响
|
||||
query.setOrderIdentifiers(null);
|
||||
query.setOrderId(null);
|
||||
query.setOrgOrderId(null);
|
||||
query.setTransferOrderId(null);
|
||||
customerKeywordResolver.resolve(query);
|
||||
String asyncTaskId = UUID.randomUUID().toString().replace("-", "");
|
||||
tabOrderCompareAsyncRunner.put(asyncTaskId, TabOrderCompareTask.STATUS_PENDING, 0, "queued",
|
||||
0, 0, 0, null, null);
|
||||
tabOrderCompareAsyncRunner.run(asyncTaskId, query);
|
||||
Map<String, Object> data = new HashMap<String, Object>();
|
||||
data.put("asyncTaskId", asyncTaskId);
|
||||
TabOrderSyncEsLog.save(billSearchStore, asyncTaskId, "COMPARE", query, data, "SUCCESS", null);
|
||||
return success(data);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:list')")
|
||||
@GetMapping("/compareTask/{asyncTaskId}")
|
||||
public AjaxResult compareTaskProgress(@PathVariable("asyncTaskId") String asyncTaskId)
|
||||
{
|
||||
TabOrderCompareTask task = tabOrderCompareAsyncRunner.get(asyncTaskId);
|
||||
if (task == null)
|
||||
{
|
||||
return error("compare task not found or expired");
|
||||
}
|
||||
return success(task);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:list')")
|
||||
@PostMapping("/checkProducts")
|
||||
public AjaxResult checkProducts(@RequestBody TabOrderMonitorQuery query)
|
||||
|
||||
@@ -47,6 +47,7 @@ import com.bms.price.domain.vo.RecalcBillPriceRequest;
|
||||
import com.bms.price.domain.vo.UpdateChargeWeightRequest;
|
||||
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.bms.price.domain.vo.OrderBillProductCheckResult;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteResult;
|
||||
import com.bms.price.enums.BillTaskSuccessStatus;
|
||||
@@ -54,6 +55,7 @@ import com.bms.price.enums.BillTaskType;
|
||||
import com.bms.price.service.IPrcBillingTaskService;
|
||||
import com.bms.price.service.IPrcOrderBillService;
|
||||
import com.bms.price.service.ITmsPriceQuoteService;
|
||||
import com.bms.price.service.OrderBillProductCheckService;
|
||||
|
||||
/**
|
||||
* Billing center: order quote trial (authenticated).
|
||||
@@ -76,6 +78,9 @@ public class BillingController extends BaseController
|
||||
@Autowired
|
||||
private IBmsBizAttachmentService bizAttachmentService;
|
||||
|
||||
@Autowired
|
||||
private OrderBillProductCheckService orderBillProductCheckService;
|
||||
|
||||
/**
|
||||
* Historical quote list.
|
||||
*/
|
||||
@@ -173,6 +178,25 @@ public class BillingController extends BaseController
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect package sales products (as of billing end time) missing from product catalog.
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:orderBill:list')")
|
||||
@PostMapping("/orderBill/checkProducts")
|
||||
public AjaxResult checkOrderBillProducts(@RequestBody PrcOrderBill query)
|
||||
{
|
||||
OrderBillProductCheckResult data = orderBillProductCheckService.check(query);
|
||||
return success(data);
|
||||
}
|
||||
|
||||
/** Drill-down order bills for one product under the same check filters. */
|
||||
@PreAuthorize("@ss.hasPermi('price:orderBill:list')")
|
||||
@PostMapping("/orderBill/checkProducts/orders")
|
||||
public AjaxResult listOrderBillsByProduct(@RequestBody PrcOrderBill query)
|
||||
{
|
||||
return success(orderBillProductCheckService.listOrdersByProduct(query));
|
||||
}
|
||||
|
||||
/**
|
||||
* Order no options for remote-search dropdown (bill list multi search).
|
||||
*/
|
||||
|
||||
+37
@@ -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);
|
||||
|
||||
|
||||
+7
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+307
-18
@@ -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();
|
||||
|
||||
+317
@@ -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 > #{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 <= #{endRow}
|
||||
)
|
||||
WHERE rn > #{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 >= #{params.beginBizDate}
|
||||
</if>
|
||||
<if test="params != null and params.endBizDate != null and params.endBizDate != ''">
|
||||
AND p.biz_date <= #{params.endBizDate}
|
||||
</if>
|
||||
<if test="params != null and params.beginPickupDate != null and params.beginPickupDate != ''">
|
||||
AND p.pickup_date >= #{params.beginPickupDate}
|
||||
</if>
|
||||
<if test="params != null and params.endPickupDate != null and params.endPickupDate != ''">
|
||||
AND p.pickup_date <= #{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 <> ''
|
||||
<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 >= #{query.params.beginBizDate}
|
||||
</if>
|
||||
<if test="query.params != null and query.params.endBizDate != null and query.params.endBizDate != ''">
|
||||
AND p.biz_date <= #{query.params.endBizDate}
|
||||
</if>
|
||||
<if test="query.params != null and query.params.beginPickupDate != null and query.params.beginPickupDate != ''">
|
||||
AND p.pickup_date >= #{query.params.beginPickupDate}
|
||||
</if>
|
||||
<if test="query.params != null and query.params.endPickupDate != null and query.params.endPickupDate != ''">
|
||||
AND p.pickup_date <= #{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 <= #{params.endBizDate}
|
||||
</if>
|
||||
<if test="params.beginPickupDate != null and params.beginPickupDate != ''">
|
||||
AND p.pickup_date >= #{params.beginPickupDate}
|
||||
</if>
|
||||
<if test="params.endPickupDate != null and params.endPickupDate != ''">
|
||||
AND p.pickup_date <= #{params.endPickupDate}
|
||||
</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
AND date_format(p.create_time,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d')
|
||||
</if>
|
||||
|
||||
@@ -13,6 +13,35 @@ export function listOrderSource(query) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 比对缺失转单号回填搜索专用(不走普通列表) */
|
||||
export function listOrderSourceByTransferNos(query) {
|
||||
const { pageNum, pageSize, ...filters } = query || {}
|
||||
return request({
|
||||
url: '/monitor/orderSource/listByTransferNos',
|
||||
method: 'post',
|
||||
params: { pageNum, pageSize },
|
||||
data: filters,
|
||||
timeout: 300000
|
||||
})
|
||||
}
|
||||
|
||||
/** 订单比对(异步):按客户+提货时间比对转单号 */
|
||||
export function compareOrderSourceByFilter(data) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/compareByFilter',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
|
||||
export function getOrderSourceCompareProgress(asyncTaskId) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/compareTask/' + asyncTaskId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function syncOrderSourceByFilter(data) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/syncByFilter',
|
||||
|
||||
@@ -65,6 +65,26 @@ export function listOrderBill(query) {
|
||||
})
|
||||
}
|
||||
|
||||
/** Order-bill product check: package sales products missing from catalog */
|
||||
export function checkOrderBillProducts(data) {
|
||||
return request({
|
||||
url: '/price/billing/orderBill/checkProducts',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 180000
|
||||
})
|
||||
}
|
||||
|
||||
/** Drill-down order bills for one product under product-check filters */
|
||||
export function listOrderBillsByProduct(data) {
|
||||
return request({
|
||||
url: '/price/billing/orderBill/checkProducts/orders',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
/** Receivable bill detail */
|
||||
export function getBill(billId) {
|
||||
return request({
|
||||
|
||||
@@ -48,27 +48,11 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品" prop="productCodeList">
|
||||
<el-select
|
||||
<el-input
|
||||
v-model="form.productCodeList"
|
||||
:multiple="!isEdit"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
reserve-keyword
|
||||
:placeholder="isEdit ? '空=全部产品' : '多选将按产品展开为多行,空=全部'"
|
||||
:remote-method="searchProduct"
|
||||
:loading="productLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="p in productOptions"
|
||||
:key="p.productCode"
|
||||
:label="(p.productName || '') + ' (' + p.productCode + ')'"
|
||||
:value="p.productCode"
|
||||
/>
|
||||
</el-select>
|
||||
:placeholder="isEdit ? '产品代码,空=全部' : '产品代码,多个用逗号/换行分隔,空=全部'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="国家" prop="countryCodeList">
|
||||
<el-select
|
||||
@@ -140,7 +124,6 @@
|
||||
|
||||
<script setup name="BizRuleConfig">
|
||||
import { listBizRule, getBizRule, addBizRule, updateBizRule, delBizRule } from '@/api/data/bizRule'
|
||||
import { listProductOptions } from '@/api/price/product'
|
||||
import { listRegionOptions } from '@/api/data/region'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
@@ -151,8 +134,6 @@ const formRef = ref()
|
||||
const open = ref(false)
|
||||
const title = ref('')
|
||||
const isEdit = ref(false)
|
||||
const productLoading = ref(false)
|
||||
const productOptions = ref([])
|
||||
const countryOptions = ref([])
|
||||
|
||||
const statusOptions = [
|
||||
@@ -223,12 +204,21 @@ function toList(v) {
|
||||
return [v]
|
||||
}
|
||||
|
||||
function parseCodes(v) {
|
||||
if (Array.isArray(v)) return v.map(x => String(x).trim()).filter(Boolean)
|
||||
if (v == null || v === '') return []
|
||||
return String(v)
|
||||
.split(/[,;\r\n,;、]+/)
|
||||
.map(x => x.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = {
|
||||
ruleId: undefined,
|
||||
ruleName: '',
|
||||
ruleType: 'THREE_SIDE',
|
||||
productCodeList: [],
|
||||
productCodeList: '',
|
||||
countryCodeList: [],
|
||||
customerCodeList: [],
|
||||
weightMin: undefined,
|
||||
@@ -238,7 +228,6 @@ function resetForm() {
|
||||
status: '0',
|
||||
remark: ''
|
||||
}
|
||||
productOptions.value = []
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
@@ -260,10 +249,6 @@ function handleUpdate(row) {
|
||||
countryCodeList: d.countryCode || '',
|
||||
customerCodeList: d.customerCode || ''
|
||||
}
|
||||
if (d.productCode) {
|
||||
productOptions.value = [{ productCode: d.productCode, productName: d.productCode }]
|
||||
searchProduct(d.productCode)
|
||||
}
|
||||
title.value = '修改规则'
|
||||
open.value = true
|
||||
loadCountries()
|
||||
@@ -272,7 +257,7 @@ function handleUpdate(row) {
|
||||
|
||||
function buildPayload() {
|
||||
const f = form.value
|
||||
const products = toList(f.productCodeList)
|
||||
const products = parseCodes(f.productCodeList)
|
||||
const countries = toList(f.countryCodeList)
|
||||
const customers = toList(f.customerCodeList)
|
||||
const base = {
|
||||
@@ -325,36 +310,6 @@ function handleDelete(ids) {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function searchProduct(keyword) {
|
||||
productLoading.value = true
|
||||
const kw = (keyword || '').trim()
|
||||
const base = { status: '1' }
|
||||
// product_code / product_name are separate likes; query both and merge
|
||||
const reqs = kw
|
||||
? [
|
||||
listProductOptions({ ...base, productCode: kw }),
|
||||
listProductOptions({ ...base, productName: kw })
|
||||
]
|
||||
: [listProductOptions(base)]
|
||||
Promise.all(reqs).then(results => {
|
||||
const map = new Map()
|
||||
results.forEach(res => {
|
||||
;(res.data || res.rows || []).forEach(p => {
|
||||
if (p && p.productCode) map.set(p.productCode, p)
|
||||
})
|
||||
})
|
||||
// keep already-selected options visible
|
||||
toList(form.value.productCodeList).forEach(code => {
|
||||
if (code && !map.has(code)) {
|
||||
const hit = productOptions.value.find(p => p.productCode === code)
|
||||
map.set(code, hit || { productCode: code, productName: code })
|
||||
}
|
||||
})
|
||||
productOptions.value = Array.from(map.values())
|
||||
}).finally(() => {
|
||||
productLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function loadCountries() {
|
||||
if (countryOptions.value.length) return
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
size="720px"
|
||||
:title="drawerTitle"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="onClosed"
|
||||
>
|
||||
<div v-loading="starting">
|
||||
<el-descriptions :column="2" border size="small" class="mb12">
|
||||
<el-descriptions-item label="任务ID" :span="2">{{ asyncTaskId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户">{{ filterText.customers }}</el-descriptions-item>
|
||||
<el-descriptions-item label="提货时间">{{ filterText.time }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag size="small" :type="statusTagType">{{ statusLabel }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="进度">{{ progress.percent || 0 }}%</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-progress
|
||||
:percentage="Math.min(100, Math.max(0, Number(progress.percent) || 0))"
|
||||
:status="progressStatus"
|
||||
:stroke-width="12"
|
||||
striped
|
||||
:striped-flow="running"
|
||||
class="mb12"
|
||||
/>
|
||||
|
||||
<div class="summary-line">
|
||||
已扫描 {{ progress.scanned || 0 }}
|
||||
/ 本地已有 {{ progress.matched || 0 }}
|
||||
/ 本地缺失 {{ progress.missing || 0 }}
|
||||
</div>
|
||||
<div v-if="progress.message" class="msg-line">{{ progress.message }}</div>
|
||||
<div v-if="progress.error" class="err-line">{{ progress.error }}</div>
|
||||
|
||||
<el-alert
|
||||
class="mb12"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="仅比对转单号:上游 TRANSFER_ORDER_ID 有、本地 transfer_no 没有。异步分页扫描,结果已写入操作记录。"
|
||||
/>
|
||||
|
||||
<div class="json-panel__head">
|
||||
<span>缺失转单号(样本)</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="!canFillSearch"
|
||||
@click="fillToSearch"
|
||||
>回填到单号搜索</el-button>
|
||||
</div>
|
||||
<el-table :data="missingRows" border size="small" max-height="360">
|
||||
<el-table-column type="index" width="55" label="#" />
|
||||
<el-table-column prop="transferNo" label="转单号" min-width="220" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<el-empty v-if="!running && !(progress.missingTransferNos || []).length" description="暂无缺失转单号" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { compareOrderSourceByFilter, getOrderSourceCompareProgress } from '@/api/monitor/orderSource'
|
||||
|
||||
const emit = defineEmits(['fill-search'])
|
||||
|
||||
const visible = ref(false)
|
||||
const starting = ref(false)
|
||||
const asyncTaskId = ref('')
|
||||
const query = ref({})
|
||||
const progress = ref({})
|
||||
let timer = null
|
||||
|
||||
const filterText = computed(() => {
|
||||
const q = query.value || {}
|
||||
const codes = Array.isArray(q.customerCodes) ? q.customerCodes.filter(Boolean) : []
|
||||
const keywords = Array.isArray(q.customerKeywords) ? q.customerKeywords.filter(Boolean) : []
|
||||
return {
|
||||
customers: keywords.join(', ') || codes.join(', ') || '-',
|
||||
time: (q.beginTime || '-') + ' ~ ' + (q.endTime || '-')
|
||||
}
|
||||
})
|
||||
|
||||
const running = computed(() => {
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
return st === 'PENDING' || st === 'RUNNING' || starting.value
|
||||
})
|
||||
|
||||
const drawerTitle = computed(() => {
|
||||
if (running.value) return '订单比对任务进行中'
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS') return '订单比对完成'
|
||||
if (st === 'FAIL') return '订单比对失败'
|
||||
return '订单比对任务'
|
||||
})
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
if (st === 'PENDING') return '排队中'
|
||||
if (st === 'RUNNING') return '扫描中'
|
||||
if (st === 'SUCCESS') return '完成'
|
||||
if (st === 'FAIL') return '失败'
|
||||
return st || '-'
|
||||
})
|
||||
|
||||
const statusTagType = computed(() => {
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS') return 'success'
|
||||
if (st === 'FAIL') return 'danger'
|
||||
if (st === 'RUNNING' || st === 'PENDING') return 'warning'
|
||||
return 'info'
|
||||
})
|
||||
|
||||
const progressStatus = computed(() => {
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS') return 'success'
|
||||
if (st === 'FAIL') return 'exception'
|
||||
return undefined
|
||||
})
|
||||
|
||||
const missingRows = computed(() => {
|
||||
const list = progress.value?.missingTransferNos || []
|
||||
return list.map(tn => ({ transferNo: tn }))
|
||||
})
|
||||
|
||||
const canFillSearch = computed(() => {
|
||||
return !running.value && (progress.value?.missingTransferNos || []).length > 0
|
||||
})
|
||||
|
||||
function fillToSearch() {
|
||||
const list = (progress.value?.missingTransferNos || [])
|
||||
.map(s => String(s || '').trim())
|
||||
.filter(Boolean)
|
||||
if (!list.length) {
|
||||
ElMessage.warning('暂无可回填的缺失转单号')
|
||||
return
|
||||
}
|
||||
const truncated = !!(progress.value?.missingTruncated) || (Number(progress.value?.missing || 0) > list.length)
|
||||
emit('fill-search', { transferNos: list, truncated })
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
function clearTimer() {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
function poll() {
|
||||
if (!asyncTaskId.value) return
|
||||
getOrderSourceCompareProgress(asyncTaskId.value).then(res => {
|
||||
progress.value = res.data || {}
|
||||
const st = (progress.value.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS' || st === 'FAIL') {
|
||||
clearTimer()
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function open(q) {
|
||||
query.value = q || {}
|
||||
asyncTaskId.value = ''
|
||||
progress.value = {}
|
||||
visible.value = true
|
||||
starting.value = true
|
||||
clearTimer()
|
||||
compareOrderSourceByFilter(query.value).then(res => {
|
||||
asyncTaskId.value = res.data?.asyncTaskId || ''
|
||||
if (!asyncTaskId.value) {
|
||||
ElMessage.error('启动比对任务失败')
|
||||
return
|
||||
}
|
||||
progress.value = { status: 'PENDING', percent: 0, message: 'queued', scanned: 0, matched: 0, missing: 0 }
|
||||
poll()
|
||||
timer = setInterval(poll, 1500)
|
||||
}).catch(() => {
|
||||
ElMessage.error('启动比对任务失败')
|
||||
}).finally(() => {
|
||||
starting.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
clearTimer()
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.summary-line {
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.msg-line {
|
||||
margin-bottom: 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.err-line {
|
||||
margin-bottom: 8px;
|
||||
color: #f56c6c;
|
||||
word-break: break-all;
|
||||
}
|
||||
.json-panel__head {
|
||||
margin: 12px 0 6px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -109,6 +109,10 @@ function actionLabel(row) {
|
||||
}
|
||||
if (action === 'ONE') return '单条同步'
|
||||
if (action === 'ONE_FAIL') return '同步失败'
|
||||
if (action === 'COMPARE') return '订单比对'
|
||||
if (action === 'COMPARE_DONE') {
|
||||
return row.status === 'FAIL' ? '比对完成(失败)' : '比对完成'
|
||||
}
|
||||
return action || '-'
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
@click="openFilterSync()"
|
||||
>同步计费</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="Search"
|
||||
v-hasPermi="['monitor:orderSource:list']"
|
||||
@click="openOrderCompare()"
|
||||
>订单比对</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="success"
|
||||
@@ -77,7 +86,7 @@
|
||||
<template #col-orderStatus="{ row }">
|
||||
{{ orderStatusLabel(row.orderStatus) }}
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<!-- <template #col-action="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
@@ -86,7 +95,7 @@
|
||||
v-hasPermi="['monitor:orderSource:sync']"
|
||||
@click="handleSyncOne(row)"
|
||||
>同步计费</el-button>
|
||||
</template>
|
||||
</template> -->
|
||||
</basic-table>
|
||||
|
||||
<order-source-sync-dialog
|
||||
@@ -96,25 +105,30 @@
|
||||
/>
|
||||
<order-source-sync-log-drawer ref="logDrawerRef" />
|
||||
<order-source-product-check-drawer ref="productCheckDrawerRef" />
|
||||
<order-source-compare-drawer ref="compareDrawerRef" @fill-search="onCompareFillSearch" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="OrderSource">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { listOrderSource, syncOrderSourceSelected } from '@/api/monitor/orderSource'
|
||||
import { listOrderSource, listOrderSourceByTransferNos, syncOrderSourceSelected } from '@/api/monitor/orderSource'
|
||||
import OrderSourceSyncDialog from './components/OrderSourceSyncDialog.vue'
|
||||
import OrderSourceSyncLogDrawer from './components/OrderSourceSyncLogDrawer.vue'
|
||||
import OrderSourceProductCheckDrawer from './components/OrderSourceProductCheckDrawer.vue'
|
||||
import OrderSourceCompareDrawer from './components/OrderSourceCompareDrawer.vue'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const syncDialogRef = ref()
|
||||
const logDrawerRef = ref()
|
||||
const productCheckDrawerRef = ref()
|
||||
const compareDrawerRef = ref()
|
||||
const checkLoading = ref(false)
|
||||
const syncingKey = ref(null)
|
||||
const batchLoading = ref(false)
|
||||
const syncFloat = ref(null)
|
||||
/** 比对回填搜索模式:走专用转单号接口,不影响普通列表 */
|
||||
const transferFillMode = ref(false)
|
||||
|
||||
const searchColumns = [
|
||||
{
|
||||
@@ -168,8 +182,8 @@ const columns = [
|
||||
{ label: '计费重', prop: 'dimensionalWeightFactor', align: 'center', width: 100 },
|
||||
{ label: '体积重', prop: 'volumeWeight', align: 'center', width: 100 },
|
||||
{ label: '实重', prop: 'weight', align: 'center', width: 100 },
|
||||
{ label: '提货时间', prop: 'pickupDate', align: 'center', width: 170 },
|
||||
{ label: '操作', prop: 'action', align: 'center', width: 120, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
{ label: '提货时间', prop: 'pickupDate', align: 'center', width: 170 }
|
||||
// { label: '操作', prop: 'action', align: 'center', width: 120, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
|
||||
const orderStatusMap = {
|
||||
@@ -183,7 +197,8 @@ const orderStatusMap = {
|
||||
7: '已上网',
|
||||
8: '已签收',
|
||||
9: '已退件',
|
||||
10: '已删除'
|
||||
10: '已删除',
|
||||
11: '已复核'
|
||||
}
|
||||
|
||||
const syncFloatVisible = computed(() => {
|
||||
@@ -247,12 +262,29 @@ function buildFilterQuery(params) {
|
||||
}
|
||||
|
||||
function queryList(params) {
|
||||
const src = params || tableRef.value?.getQueryParams?.() || {}
|
||||
const identifiers = splitLines(src.orderIdentifiers)
|
||||
if (!identifiers.length) transferFillMode.value = false
|
||||
|
||||
// 回填搜索:走专用接口,不走普通 /list
|
||||
if (transferFillMode.value && identifiers.length) {
|
||||
if (identifiers.length > MAX_ORDER_IDENTIFIERS) {
|
||||
ElMessage.warning(`单次最多查询 ${MAX_ORDER_IDENTIFIERS} 个单号,当前 ${identifiers.length} 个,请减少后再查`)
|
||||
return Promise.resolve({ rows: [], total: 0 })
|
||||
}
|
||||
const customers = splitLines(src.customerKeywords)
|
||||
const fillQuery = { orderIdentifiers: identifiers }
|
||||
if (customers.length) fillQuery.customerKeywords = customers
|
||||
fillQuery.pageNum = params.pageNum
|
||||
fillQuery.pageSize = params.pageSize
|
||||
return listOrderSourceByTransferNos(fillQuery)
|
||||
}
|
||||
|
||||
const query = buildFilterQuery(params)
|
||||
const identifierCount = (query.orderIdentifiers || []).length
|
||||
const hasIdentifier = identifierCount > 0
|
||||
const hasCustomer = !!(query.customerKeywords && query.customerKeywords.length)
|
||||
const hasTime = !!query.beginTime && !!query.endTime
|
||||
// 有任一单号即可查询;无单号时必须客户+提货时间
|
||||
if (!hasIdentifier && !(hasCustomer && hasTime)) {
|
||||
ElMessage.warning('请输入单号,或选择客户和提货时间')
|
||||
return Promise.resolve({ rows: [], total: 0 })
|
||||
@@ -266,6 +298,52 @@ function queryList(params) {
|
||||
return listOrderSource(query)
|
||||
}
|
||||
|
||||
function openOrderCompare() {
|
||||
const q = buildFilterQuery()
|
||||
const hasCustomer = !!(q.customerKeywords && q.customerKeywords.length)
|
||||
const hasTime = !!q.beginTime && !!q.endTime
|
||||
if (!hasCustomer || !hasTime) {
|
||||
ElMessage.warning('订单比对需先输入客户并选择提货时间')
|
||||
return
|
||||
}
|
||||
proxy.$modal.confirm(
|
||||
'将按当前客户与提货时间,异步分页比对上游转单号与本地订单库(仅转单号),确认继续?',
|
||||
'二次确认'
|
||||
).then(() => {
|
||||
// 比对只按客户+时间;不带单号框内容(回填后可能有数千转单号,带上会把比对拖死)
|
||||
const compareQuery = { ...q }
|
||||
delete compareQuery.orderIdentifiers
|
||||
delete compareQuery.orderId
|
||||
delete compareQuery.orgOrderId
|
||||
delete compareQuery.transferOrderId
|
||||
compareDrawerRef.value?.open(compareQuery)
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
/** 比对缺失转单号回填到列表「单号」搜索框并查询 */
|
||||
function onCompareFillSearch(payload) {
|
||||
const list = (payload && payload.transferNos) || []
|
||||
if (!list.length) {
|
||||
ElMessage.warning('暂无可回填的转单号')
|
||||
return
|
||||
}
|
||||
let nos = list
|
||||
if (nos.length > MAX_ORDER_IDENTIFIERS) {
|
||||
nos = nos.slice(0, MAX_ORDER_IDENTIFIERS)
|
||||
ElMessage.warning(`单次最多查询 ${MAX_ORDER_IDENTIFIERS} 个单号,已截取前 ${MAX_ORDER_IDENTIFIERS} 个`)
|
||||
} else if (payload && payload.truncated) {
|
||||
ElMessage.warning(`缺失转单号较多,仅回填样本 ${nos.length} 个`)
|
||||
} else {
|
||||
ElMessage.success(`已回填 ${nos.length} 个转单号并开始搜索`)
|
||||
}
|
||||
transferFillMode.value = true
|
||||
tableRef.value?.setQueryParams?.({
|
||||
orderIdentifiers: nos.join('\n'),
|
||||
pageNum: 1
|
||||
})
|
||||
tableRef.value?.handleQuery?.()
|
||||
}
|
||||
|
||||
function openFilterSync() {
|
||||
const q = buildFilterQuery()
|
||||
const identifierCount = (q.orderIdentifiers || []).length
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" size="920px" title="产品库比对(套餐销售价)" append-to-body destroy-on-close>
|
||||
<div v-loading="loading">
|
||||
<el-empty v-if="!loading && !result" description="暂无结果" />
|
||||
<template v-else-if="result">
|
||||
<el-descriptions :column="3" border size="small" class="mb12">
|
||||
<el-descriptions-item label="筛选订单数">{{ result.orderTotal ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="套餐截止时间">{{ result.asOfTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="套餐产品数">{{ result.packageProductCount ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="库内/缺失">
|
||||
<el-tag type="success" size="small">{{ result.matchedCount ?? 0 }}</el-tag>
|
||||
<span class="sep">/</span>
|
||||
<el-tag type="danger" size="small">{{ result.missingCount ?? 0 }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane :label="missingTabLabel" name="missing">
|
||||
<el-table :data="result.missingProducts || []" border size="small" max-height="520">
|
||||
<el-table-column prop="customerCode" label="客户代码" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="productCode" label="套餐产品代码" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="productName" label="产品名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="orderCount" label="订单数" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :title="'点击查订单'" @click="openOrders(row)">{{ row.orderCount ?? 0 }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!(result.missingProducts || []).length" description="未发现产品库缺失" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="packageTabLabel" name="package">
|
||||
<el-table :data="result.packageProducts || []" border size="small" max-height="520">
|
||||
<el-table-column prop="customerCode" label="客户代码" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="productCode" label="套餐产品代码" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="productName" label="产品名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="产品库" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.inLibrary" size="small" type="success">在库</el-tag>
|
||||
<el-tag v-else size="small" type="danger">不在库</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderCount" label="订单数" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openOrders(row)">{{ row.orderCount ?? 0 }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!(result.packageProducts || []).length" description="客户未绑定套餐产品" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="orderDialogVisible" :title="orderDialogTitle" width="860px" append-to-body destroy-on-close>
|
||||
<el-table v-loading="orderLoading" :data="orderRows" border size="small" max-height="480">
|
||||
<el-table-column prop="orderNo" label="订单号" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="trackingNo" label="运单号" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="transferNo" label="转单号" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="bizDate" label="计费时间" width="170" align="center" />
|
||||
</el-table>
|
||||
<el-empty v-if="!orderLoading && !orderRows.length" description="未查到订单" />
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { checkOrderBillProducts, listOrderBillsByProduct } from '@/api/price/billing'
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const result = ref(null)
|
||||
const activeTab = ref('missing')
|
||||
const lastQuery = ref(null)
|
||||
|
||||
const orderDialogVisible = ref(false)
|
||||
const orderLoading = ref(false)
|
||||
const orderRows = ref([])
|
||||
const orderDialogTitle = ref('产品订单')
|
||||
|
||||
const missingTabLabel = computed(() => {
|
||||
const n = result.value?.missingCount ?? 0
|
||||
return '缺失产品 (' + n + ')'
|
||||
})
|
||||
const packageTabLabel = computed(() => {
|
||||
const n = result.value?.packageProductCount ?? 0
|
||||
return '套餐产品 (' + n + ')'
|
||||
})
|
||||
|
||||
function openOrders(row) {
|
||||
if (!row || !row.productCode || !lastQuery.value) {
|
||||
ElMessage.warning('请先选择客户并选择计费时间')
|
||||
return
|
||||
}
|
||||
const q = {
|
||||
...lastQuery.value,
|
||||
customerCodes: row.customerCode ? [row.customerCode] : lastQuery.value.customerCodes,
|
||||
productCode: row.productCode
|
||||
}
|
||||
orderDialogTitle.value = '产品订单: ' + row.productCode
|
||||
orderDialogVisible.value = true
|
||||
orderRows.value = []
|
||||
orderLoading.value = true
|
||||
listOrderBillsByProduct(q).then(res => {
|
||||
orderRows.value = Array.isArray(res.data) ? res.data : []
|
||||
}).catch(() => {
|
||||
orderRows.value = []
|
||||
}).finally(() => {
|
||||
orderLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function open(query) {
|
||||
const hasCustomer = !!(query && Array.isArray(query.customerCodes) && query.customerCodes.length)
|
||||
const begin = query?.params?.beginBizDate
|
||||
const end = query?.params?.endBizDate
|
||||
if (!query || !hasCustomer || !begin || !end) {
|
||||
ElMessage.warning('请先选择客户并选择计费时间')
|
||||
return
|
||||
}
|
||||
lastQuery.value = { ...query }
|
||||
visible.value = true
|
||||
activeTab.value = 'missing'
|
||||
result.value = null
|
||||
loading.value = true
|
||||
checkOrderBillProducts(query).then(res => {
|
||||
result.value = res.data || null
|
||||
if ((result.value?.missingCount || 0) > 0) {
|
||||
activeTab.value = 'missing'
|
||||
} else {
|
||||
activeTab.value = 'package'
|
||||
}
|
||||
}).catch(() => {
|
||||
result.value = null
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.sep { margin: 0 6px; color: var(--el-text-color-secondary); }
|
||||
</style>
|
||||
@@ -24,6 +24,16 @@
|
||||
@click="openFilterRebill()"
|
||||
>重新计费</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
icon="Search"
|
||||
:loading="checkLoading"
|
||||
v-hasPermi="['price:orderBill:list']"
|
||||
@click="handleCheckProducts()"
|
||||
>检测产品</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="Document" @click="logDrawerRef.open()">操作记录</el-button>
|
||||
</el-col>
|
||||
@@ -83,6 +93,7 @@
|
||||
</basic-table>
|
||||
<order-source-sync-dialog ref="syncDialogRef" @success="onRebillSuccess" />
|
||||
<order-source-sync-log-drawer ref="logDrawerRef" />
|
||||
<order-bill-product-check-drawer ref="productCheckDrawerRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -92,10 +103,13 @@ import { listOrderBill, listOrderNoOptions, listTrackingNoOptions, listTransferN
|
||||
import { syncOrderSourceSelected } from '@/api/monitor/orderSource'
|
||||
import OrderSourceSyncDialog from '../../monitor/orderSource/components/OrderSourceSyncDialog.vue'
|
||||
import OrderSourceSyncLogDrawer from '../../monitor/orderSource/components/OrderSourceSyncLogDrawer.vue'
|
||||
import OrderBillProductCheckDrawer from './components/OrderBillProductCheckDrawer.vue'
|
||||
|
||||
const tableRef = ref()
|
||||
const syncDialogRef = ref()
|
||||
const logDrawerRef = ref()
|
||||
const productCheckDrawerRef = ref()
|
||||
const checkLoading = ref(false)
|
||||
const batchLoading = ref(false)
|
||||
const tableHeight = ref(480)
|
||||
let tableResizeObserver = null
|
||||
@@ -183,6 +197,16 @@ const searchColumns = [
|
||||
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '提货时间',
|
||||
prop: 'pickupDateRange',
|
||||
component: 'datetimerange',
|
||||
colSpan: 2,
|
||||
props: {
|
||||
clearable: true,
|
||||
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '订单状态',
|
||||
prop: 'orderStatuses',
|
||||
@@ -205,17 +229,26 @@ const searchColumns = [
|
||||
}
|
||||
]
|
||||
|
||||
function queryList(params) {
|
||||
const { bizDateRange, ...rest } = params
|
||||
function applySearchRanges(params) {
|
||||
const { bizDateRange, pickupDateRange, ...rest } = params
|
||||
const query = { ...rest }
|
||||
const rangeParams = {}
|
||||
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
|
||||
query.params = {
|
||||
...(query.params || {}),
|
||||
beginBizDate: bizDateRange[0] || null,
|
||||
endBizDate: bizDateRange[1] || null
|
||||
}
|
||||
rangeParams.beginBizDate = bizDateRange[0] || null
|
||||
rangeParams.endBizDate = bizDateRange[1] || null
|
||||
}
|
||||
return listOrderBill(query)
|
||||
if (Array.isArray(pickupDateRange) && (pickupDateRange[0] || pickupDateRange[1])) {
|
||||
rangeParams.beginPickupDate = pickupDateRange[0] || null
|
||||
rangeParams.endPickupDate = pickupDateRange[1] || null
|
||||
}
|
||||
if (Object.keys(rangeParams).length) {
|
||||
query.params = { ...(query.params || {}), ...rangeParams }
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
function queryList(params) {
|
||||
return listOrderBill(applySearchRanges(params))
|
||||
}
|
||||
|
||||
|
||||
@@ -237,6 +270,29 @@ function buildRebillFilter() {
|
||||
return filter
|
||||
}
|
||||
|
||||
|
||||
function buildCheckQuery() {
|
||||
const q = tableRef.value?.getQueryParams?.() || {}
|
||||
return applySearchRanges(q)
|
||||
}
|
||||
|
||||
function handleCheckProducts() {
|
||||
const q = buildCheckQuery()
|
||||
const hasCustomer = Array.isArray(q.customerCodes) && q.customerCodes.length > 0
|
||||
const begin = q.params?.beginBizDate
|
||||
const end = q.params?.endBizDate
|
||||
if (!hasCustomer || !begin || !end) {
|
||||
ElMessage.warning('检测产品需先选择客户并选择计费时间')
|
||||
return
|
||||
}
|
||||
checkLoading.value = true
|
||||
try {
|
||||
productCheckDrawerRef.value?.open(q)
|
||||
} finally {
|
||||
checkLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openFilterRebill() {
|
||||
const identifiers = currentIdentifiers()
|
||||
if (!identifiers.length) {
|
||||
|
||||
Reference in New Issue
Block a user