增加代码检测,和映射

This commit is contained in:
wuweihua
2026-10-13 13:39:15 +08:00
parent b40033f424
commit 52816ad618
22 changed files with 1573 additions and 0 deletions
@@ -0,0 +1,73 @@
package com.bms.web.controller.data;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.bms.common.annotation.Log;
import com.bms.common.core.controller.BaseController;
import com.bms.common.core.domain.AjaxResult;
import com.bms.common.core.page.TableDataInfo;
import com.bms.common.enums.BusinessType;
import com.bms.system.domain.BmsProductCodeMapping;
import com.bms.system.service.IBmsProductCodeMappingService;
/**
* 代码映射:上游产品代码 -> BMS产品代码
*/
@RestController
@RequestMapping("/data/productCodeMap")
public class BmsProductCodeMappingController extends BaseController
{
@Autowired
private IBmsProductCodeMappingService mappingService;
@PreAuthorize("@ss.hasPermi('data:productCodeMap:list')")
@GetMapping("/list")
public TableDataInfo list(BmsProductCodeMapping query)
{
startPage();
List<BmsProductCodeMapping> list = mappingService.selectList(query);
return getDataTable(list);
}
@PreAuthorize("@ss.hasPermi('data:productCodeMap:query')")
@GetMapping("/{mapId}")
public AjaxResult getInfo(@PathVariable Long mapId)
{
return success(mappingService.selectById(mapId));
}
@PreAuthorize("@ss.hasPermi('data:productCodeMap:add')")
@Log(title = "product code mapping", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody BmsProductCodeMapping row)
{
row.setCreateBy(getUsername());
return toAjax(mappingService.insert(row));
}
@PreAuthorize("@ss.hasPermi('data:productCodeMap:edit')")
@Log(title = "product code mapping", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody BmsProductCodeMapping row)
{
row.setUpdateBy(getUsername());
return toAjax(mappingService.update(row));
}
@PreAuthorize("@ss.hasPermi('data:productCodeMap:remove')")
@Log(title = "product code mapping", businessType = BusinessType.DELETE)
@DeleteMapping("/{mapIds}")
public AjaxResult remove(@PathVariable Long[] mapIds)
{
return toAjax(mappingService.deleteByIds(mapIds));
}
}
@@ -26,7 +26,9 @@ import com.bms.common.utils.StringUtils;
import com.bms.price.domain.PrcBillingTaskRetryTask;
import com.bms.price.domain.vo.TabOrderMonitorQuery;
import com.bms.price.domain.vo.TabOrderMonitorVo;
import com.bms.price.domain.vo.TabOrderProductCheckResult;
import com.bms.price.oracle.TabOrderMonitorService;
import com.bms.price.oracle.TabOrderProductCheckService;
import com.bms.price.search.BillSearchPage;
import com.bms.price.search.EsLogType;
import com.bms.price.search.IBillSearchStore;
@@ -43,6 +45,9 @@ public class TabOrderMonitorController extends BaseController
@Autowired
private TabOrderMonitorService tabOrderMonitorService;
@Autowired
private TabOrderProductCheckService tabOrderProductCheckService;
@Autowired
private TabOrderSyncAsyncRunner tabOrderSyncAsyncRunner;
@@ -74,6 +79,18 @@ public class TabOrderMonitorController extends BaseController
return rsp;
}
@PreAuthorize("@ss.hasPermi('monitor:orderSource:list')")
@PostMapping("/checkProducts")
public AjaxResult checkProducts(@RequestBody TabOrderMonitorQuery query)
{
if (!hasRequiredFilters(query))
{
return error("customerCodes and pickup time required");
}
TabOrderProductCheckResult data = tabOrderProductCheckService.check(query);
return success(data);
}
@PreAuthorize("@ss.hasPermi('monitor:orderSource:sync')")
@Log(title = "order source sync filter", businessType = BusinessType.UPDATE)
@PostMapping("/syncByFilter")
@@ -0,0 +1,277 @@
package com.bms.price.domain.vo;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Order-source product vs customer package comparison result.
*/
public class TabOrderProductCheckResult implements Serializable
{
private static final long serialVersionUID = 1L;
private long orderTotal;
private int scannedOrders;
private int distinctOrderProducts;
private int packageProductCount;
private int matchedCount;
private int missingCount;
private String asOfTime;
private boolean truncated;
private List<PackageProductRow> packageProducts = new ArrayList<PackageProductRow>();
private List<OrderProductRow> orderProducts = new ArrayList<OrderProductRow>();
private List<OrderProductRow> missingProducts = new ArrayList<OrderProductRow>();
public long getOrderTotal()
{
return orderTotal;
}
public void setOrderTotal(long orderTotal)
{
this.orderTotal = orderTotal;
}
public int getScannedOrders()
{
return scannedOrders;
}
public void setScannedOrders(int scannedOrders)
{
this.scannedOrders = scannedOrders;
}
public int getDistinctOrderProducts()
{
return distinctOrderProducts;
}
public void setDistinctOrderProducts(int distinctOrderProducts)
{
this.distinctOrderProducts = distinctOrderProducts;
}
public int getPackageProductCount()
{
return packageProductCount;
}
public void setPackageProductCount(int packageProductCount)
{
this.packageProductCount = packageProductCount;
}
public int getMatchedCount()
{
return matchedCount;
}
public void setMatchedCount(int matchedCount)
{
this.matchedCount = matchedCount;
}
public int getMissingCount()
{
return missingCount;
}
public void setMissingCount(int missingCount)
{
this.missingCount = missingCount;
}
public String getAsOfTime()
{
return asOfTime;
}
public void setAsOfTime(String asOfTime)
{
this.asOfTime = asOfTime;
}
public boolean isTruncated()
{
return truncated;
}
public void setTruncated(boolean truncated)
{
this.truncated = truncated;
}
public List<PackageProductRow> getPackageProducts()
{
return packageProducts;
}
public void setPackageProducts(List<PackageProductRow> packageProducts)
{
this.packageProducts = packageProducts;
}
public List<OrderProductRow> getOrderProducts()
{
return orderProducts;
}
public void setOrderProducts(List<OrderProductRow> orderProducts)
{
this.orderProducts = orderProducts;
}
public List<OrderProductRow> getMissingProducts()
{
return missingProducts;
}
public void setMissingProducts(List<OrderProductRow> missingProducts)
{
this.missingProducts = missingProducts;
}
public static class PackageProductRow implements Serializable
{
private static final long serialVersionUID = 1L;
private String customerCode;
private String productCode;
private String productName;
public String getCustomerCode()
{
return customerCode;
}
public void setCustomerCode(String customerCode)
{
this.customerCode = customerCode;
}
public String getProductCode()
{
return productCode;
}
public void setProductCode(String productCode)
{
this.productCode = productCode;
}
public String getProductName()
{
return productName;
}
public void setProductName(String productName)
{
this.productName = productName;
}
}
public static class OrderProductRow implements Serializable
{
private static final long serialVersionUID = 1L;
private String customerCode;
private String upstreamCode;
private String productName;
private String mappedCode;
private boolean inPackage;
private boolean mappedInPackage;
private boolean covered;
private int orderCount;
private List<String> sampleOrderIds = new ArrayList<String>();
public String getCustomerCode()
{
return customerCode;
}
public void setCustomerCode(String customerCode)
{
this.customerCode = customerCode;
}
public String getUpstreamCode()
{
return upstreamCode;
}
public void setUpstreamCode(String upstreamCode)
{
this.upstreamCode = upstreamCode;
}
public String getProductName()
{
return productName;
}
public void setProductName(String productName)
{
this.productName = productName;
}
public String getMappedCode()
{
return mappedCode;
}
public void setMappedCode(String mappedCode)
{
this.mappedCode = mappedCode;
}
public boolean isInPackage()
{
return inPackage;
}
public void setInPackage(boolean inPackage)
{
this.inPackage = inPackage;
}
public boolean isMappedInPackage()
{
return mappedInPackage;
}
public void setMappedInPackage(boolean mappedInPackage)
{
this.mappedInPackage = mappedInPackage;
}
public boolean isCovered()
{
return covered;
}
public void setCovered(boolean covered)
{
this.covered = covered;
}
public int getOrderCount()
{
return orderCount;
}
public void setOrderCount(int orderCount)
{
this.orderCount = orderCount;
}
public List<String> getSampleOrderIds()
{
return sampleOrderIds;
}
public void setSampleOrderIds(List<String> sampleOrderIds)
{
this.sampleOrderIds = sampleOrderIds;
}
}
}
@@ -27,4 +27,9 @@ public interface OracleUpstreamOrderMapper
@Param("query") TabOrderMonitorQuery query,
@Param("startRow") long startRow,
@Param("endRow") long endRow);
/** Distinct product codes for product-vs-package check (grouped, no pagination). */
List<Map<String, Object>> selectTabOrderProductCodes(
@Param("orderTable") String orderTable,
@Param("query") TabOrderMonitorQuery query);
}
@@ -40,6 +40,7 @@ public class OracleOrderBillMapper
{
product = defaultProductCode;
}
// Keep upstream product code; package miss then maps via product_code_mapping in doQuote.
req.setProductCode(product);
req.setCountryCode(firstStr(row, "COUNTRY"));
String countryName = firstStr(row, "COUNTRY_NAME", "COUNTRY_EINGTH_NAME", "COUNTRY");
@@ -55,6 +55,32 @@ public class TabOrderMonitorService
}
}
@DataSource(DataSourceType.SLAVE)
public List<Map<String, Object>> selectDistinctProductCodes(TabOrderMonitorQuery query)
{
validate(query);
String table = resolveTable();
try
{
PageHelper.clearPage();
long t0 = System.currentTimeMillis();
List<Map<String, Object>> rows = oracleUpstreamOrderMapper.selectTabOrderProductCodes(table, query);
log.info("oracle tab_order product codes customerSize={} begin={} end={} groups={} costMs={}",
query.getCustomerCodes() == null ? 0 : query.getCustomerCodes().size(),
query.getBeginTime(), query.getEndTime(),
rows == null ? 0 : rows.size(), System.currentTimeMillis() - t0);
return rows == null ? Collections.emptyList() : rows;
}
catch (RuntimeException e)
{
throw new ServiceException("Oracle TAB_ORDER product codes failed: " + e.getMessage());
}
finally
{
PageHelper.clearPage();
}
}
@DataSource(DataSourceType.SLAVE)
public List<TabOrderMonitorVo> selectPage(TabOrderMonitorQuery query, int pageNum, int pageSize)
{
@@ -0,0 +1,284 @@
package com.bms.price.oracle;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
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.utils.StringUtils;
import com.bms.price.domain.PrcPriceSetProduct;
import com.bms.price.domain.vo.TabOrderMonitorQuery;
import com.bms.price.domain.vo.TabOrderProductCheckResult;
import com.bms.price.domain.vo.TabOrderProductCheckResult.OrderProductRow;
import com.bms.price.domain.vo.TabOrderProductCheckResult.PackageProductRow;
import com.bms.price.service.ITmsPriceQuoteService;
import com.bms.system.service.IBmsProductCodeMappingService;
/**
* Compare distinct TAB_ORDER product codes against customer package products.
*/
@Service
public class TabOrderProductCheckService
{
@Autowired
private TabOrderMonitorService tabOrderMonitorService;
@Autowired
private ITmsPriceQuoteService tmsPriceQuoteService;
@Autowired
private IBmsProductCodeMappingService productCodeMappingService;
public TabOrderProductCheckResult check(TabOrderMonitorQuery query)
{
Date asOf = resolveAsOf(query);
TabOrderProductCheckResult result = new TabOrderProductCheckResult();
result.setAsOfTime(formatAsOf(asOf));
result.setTruncated(false);
List<Map<String, Object>> groups = tabOrderMonitorService.selectDistinctProductCodes(query);
if (groups == null || groups.isEmpty())
{
result.setOrderTotal(0L);
result.setScannedOrders(0);
return result;
}
long orderTotal = 0L;
Set<String> customerCodes = new HashSet<String>();
List<OrderProductRow> orderProducts = new ArrayList<OrderProductRow>();
List<OrderProductRow> missingProducts = new ArrayList<OrderProductRow>();
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;
}
customerCodes.add(customerCode);
orderTotal += longValue(g, "ORDER_COUNT");
}
Map<String, Set<String>> packageCodeByCustomer = new HashMap<String, Set<String>>();
List<PackageProductRow> packageRows = new ArrayList<PackageProductRow>();
for (String customerCode : customerCodes)
{
List<PrcPriceSetProduct> products = tmsPriceQuoteService.listAllBoundPackageProducts(customerCode, asOf);
Set<String> codes = new HashSet<String>();
if (products != null)
{
for (PrcPriceSetProduct p : products)
{
if (p == null || StringUtils.isEmpty(p.getProductCode()))
{
continue;
}
String code = p.getProductCode().trim();
String upper = code.toUpperCase(Locale.ROOT);
if (!codes.add(upper))
{
continue;
}
PackageProductRow pr = new PackageProductRow();
pr.setCustomerCode(customerCode);
pr.setProductCode(code);
pr.setProductName(trim(p.getProductName()));
packageRows.add(pr);
}
}
packageCodeByCustomer.put(customerCode, codes);
}
packageRows.sort(Comparator
.comparing(PackageProductRow::getCustomerCode, nullSafe())
.thenComparing(PackageProductRow::getProductCode, nullSafe()));
int matched = 0;
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;
}
Set<String> pkg = packageCodeByCustomer.get(customerCode);
if (pkg == null)
{
pkg = Collections.emptySet();
}
boolean inPackage = pkg.contains(productCode.toUpperCase(Locale.ROOT));
String mappedCode = resolveMappedIfDifferent(productCode);
boolean mappedInPackage = false;
if (StringUtils.isNotEmpty(mappedCode))
{
mappedInPackage = pkg.contains(mappedCode.toUpperCase(Locale.ROOT));
}
boolean covered = inPackage || mappedInPackage;
OrderProductRow row = new OrderProductRow();
row.setCustomerCode(customerCode);
row.setUpstreamCode(productCode);
row.setProductName(trim(str(g, "PRODUCT_NAME")));
row.setMappedCode(mappedCode);
row.setInPackage(inPackage);
row.setMappedInPackage(mappedInPackage);
row.setCovered(covered);
row.setOrderCount((int) Math.min(Integer.MAX_VALUE, longValue(g, "ORDER_COUNT")));
orderProducts.add(row);
if (covered)
{
matched++;
}
else
{
missingProducts.add(row);
}
}
orderProducts.sort(Comparator
.comparing(OrderProductRow::isCovered)
.thenComparing(OrderProductRow::getCustomerCode, nullSafe())
.thenComparing(OrderProductRow::getUpstreamCode, nullSafe()));
missingProducts.sort(Comparator
.comparing(OrderProductRow::getCustomerCode, nullSafe())
.thenComparing(OrderProductRow::getUpstreamCode, nullSafe()));
result.setOrderTotal(orderTotal);
result.setScannedOrders((int) Math.min(Integer.MAX_VALUE, orderTotal));
result.setDistinctOrderProducts(orderProducts.size());
result.setPackageProductCount(packageRows.size());
result.setMatchedCount(matched);
result.setMissingCount(missingProducts.size());
result.setPackageProducts(packageRows);
result.setOrderProducts(orderProducts);
result.setMissingProducts(missingProducts);
return result;
}
private String resolveMappedIfDifferent(String upstreamCode)
{
if (productCodeMappingService == null || StringUtils.isEmpty(upstreamCode))
{
return null;
}
String mapped = productCodeMappingService.resolveBmsProductCode(upstreamCode);
if (StringUtils.isEmpty(mapped) || mapped.equalsIgnoreCase(upstreamCode.trim()))
{
return null;
}
return mapped.trim();
}
private static Date resolveAsOf(TabOrderMonitorQuery query)
{
String raw = query == null ? null : query.getEndTime();
if (StringUtils.isEmpty(raw) && query != null)
{
raw = query.getBeginTime();
}
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 trim(String s)
{
return StringUtils.isEmpty(s) ? null : s.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;
}
}
@@ -36,4 +36,11 @@ public interface ITmsPriceQuoteService
* @param bizDate optional; default today (same as-of window as quote)
*/
List<PrcPriceSetProduct> listBoundPackageProducts(String customerCode, Date bizDate);
/**
* Union of products from all effective bound packages (for order-source product check).
* @param customerCode required
* @param bizDate optional; default today
*/
List<PrcPriceSetProduct> listAllBoundPackageProducts(String customerCode, Date bizDate);
}
@@ -9,6 +9,7 @@ import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
@@ -57,6 +58,7 @@ import com.bms.price.search.EsBizLogSupport;
import com.bms.price.search.EsLogType;
import com.bms.price.search.IBillSearchStore;
import com.bms.price.service.ITmsPriceQuoteService;
import com.bms.system.service.IBmsProductCodeMappingService;
/**
* Shared quote/bill calc: customer bind + package version by bizDate (version effective_time).
@@ -103,6 +105,9 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
@Autowired
private BmsProductMapper productMapper;
@Autowired(required = false)
private IBmsProductCodeMappingService productCodeMappingService;
@Autowired
private PrcSurchargeRelMapper surchargeRelMapper;
@@ -261,6 +266,52 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
return Collections.emptyList();
}
@Override
public List<PrcPriceSetProduct> listAllBoundPackageProducts(String customerCode, Date bizDate)
{
if (StringUtils.isEmpty(customerCode))
{
return Collections.emptyList();
}
List<PrcCustomerPriceSet> binds = customerPriceSetMapper.selectEffectiveByCustomerCode(customerCode.trim());
if (binds == null || binds.isEmpty())
{
return Collections.emptyList();
}
Date asOf = bizDate != null ? bizDate : new Date();
Map<String, PrcPriceSetProduct> byCode = new LinkedHashMap<String, PrcPriceSetProduct>();
for (PrcCustomerPriceSet bind : binds)
{
if (bind == null || bind.getPriceSetId() == null)
{
continue;
}
PrcPriceSet set = priceSetMapper.selectPrcPriceSetHeaderById(bind.getPriceSetId());
if (set == null || !"E".equals(set.getStatus()))
{
continue;
}
List<PrcPriceSetProduct> products = loadProductsForPackage(set.getPriceSetId(), asOf);
if (products == null || products.isEmpty())
{
continue;
}
for (PrcPriceSetProduct p : products)
{
if (p == null || StringUtils.isEmpty(p.getProductCode()))
{
continue;
}
String key = p.getProductCode().trim().toUpperCase(Locale.ROOT);
if (!byCode.containsKey(key))
{
byCode.put(key, p);
}
}
}
return new ArrayList<PrcPriceSetProduct>(byCode.values());
}
/** Prefer version active at asOf; else latest version that has set-product rows. */
private List<PrcPriceSetProduct> loadProductsForPackage(Long priceSetId, Date asOf)
{
@@ -373,6 +424,25 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
PrcPriceSetProduct setProduct = resolveSetProduct(version.getPsvId(), productCode);
if (setProduct == null)
{
// Package has no this product: try product_code_mapping (upstream -> BMS).
String mappedCode = resolveMappedProductCode(productCode);
if (mappedCode != null)
{
setProduct = resolveSetProduct(version.getPsvId(), mappedCode);
if (setProduct != null)
{
productCode = mappedCode;
request.setProductCode(mappedCode);
String mappedName = resolveProductName(mappedCode, bizDate);
if (StringUtils.isNotEmpty(mappedName))
{
productName = mappedName;
}
}
}
}
if (setProduct == null)
{
PrcPriceSetProduct probe = new PrcPriceSetProduct();
probe.setPsvId(version.getPsvId());
@@ -470,6 +540,23 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
return setProductMapper.selectByPsvIdAndCode(psvId, productCode);
}
/**
* Return mapped BMS product code only when mapping exists and differs from upstream code.
*/
private String resolveMappedProductCode(String upstreamCode)
{
if (productCodeMappingService == null || StringUtils.isEmpty(upstreamCode))
{
return null;
}
String mapped = productCodeMappingService.resolveBmsProductCode(upstreamCode);
if (StringUtils.isEmpty(mapped) || mapped.equalsIgnoreCase(upstreamCode.trim()))
{
return null;
}
return mapped.trim();
}
private static final class ZoneResolve
{
private boolean required;
@@ -0,0 +1,92 @@
package com.bms.system.domain;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.bms.common.annotation.Excel;
import com.bms.common.core.domain.BaseEntity;
/**
* 产品代码映射 product_code_mapping
*/
public class BmsProductCodeMapping extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long mapId;
/** 上游产品代码 */
@Excel(name = "上游产品代码")
private String upstreamCode;
/** BMS产品代码 */
@Excel(name = "BMS产品代码")
private String bmsProductCode;
/** 状态(0正常 1停用) */
@Excel(name = "状态", readConverterExp = "0=正常,1=停用")
private String status;
private String delFlag;
public Long getMapId()
{
return mapId;
}
public void setMapId(Long mapId)
{
this.mapId = mapId;
}
public String getUpstreamCode()
{
return upstreamCode;
}
public void setUpstreamCode(String upstreamCode)
{
this.upstreamCode = upstreamCode;
}
public String getBmsProductCode()
{
return bmsProductCode;
}
public void setBmsProductCode(String bmsProductCode)
{
this.bmsProductCode = bmsProductCode;
}
public String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
public String getDelFlag()
{
return delFlag;
}
public void setDelFlag(String delFlag)
{
this.delFlag = delFlag;
}
@Override
public String toString()
{
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("mapId", getMapId())
.append("upstreamCode", getUpstreamCode())
.append("bmsProductCode", getBmsProductCode())
.append("status", getStatus())
.append("remark", getRemark())
.toString();
}
}
@@ -0,0 +1,23 @@
package com.bms.system.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.system.domain.BmsProductCodeMapping;
public interface BmsProductCodeMappingMapper
{
List<BmsProductCodeMapping> selectList(BmsProductCodeMapping query);
BmsProductCodeMapping selectById(Long mapId);
/** Active mapping by upstream code (status=0, del_flag=0). */
BmsProductCodeMapping selectActiveByUpstreamCode(@Param("upstreamCode") String upstreamCode);
int countByUpstreamCode(@Param("upstreamCode") String upstreamCode, @Param("excludeId") Long excludeId);
int insert(BmsProductCodeMapping row);
int update(BmsProductCodeMapping row);
int deleteByIds(Long[] mapIds);
}
@@ -0,0 +1,24 @@
package com.bms.system.service;
import java.util.List;
import com.bms.system.domain.BmsProductCodeMapping;
public interface IBmsProductCodeMappingService
{
List<BmsProductCodeMapping> selectList(BmsProductCodeMapping query);
BmsProductCodeMapping selectById(Long mapId);
int insert(BmsProductCodeMapping row);
int update(BmsProductCodeMapping row);
int deleteByIds(Long[] mapIds);
/**
* Resolve BMS product code from upstream code.
* Used after package product miss: returns mapped code when active mapping exists;
* otherwise returns original code.
*/
String resolveBmsProductCode(String upstreamCode);
}
@@ -0,0 +1,107 @@
package com.bms.system.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.StringUtils;
import com.bms.system.domain.BmsProductCodeMapping;
import com.bms.system.mapper.BmsProductCodeMappingMapper;
import com.bms.system.service.IBmsProductCodeMappingService;
@Service
public class BmsProductCodeMappingServiceImpl implements IBmsProductCodeMappingService
{
@Autowired
private BmsProductCodeMappingMapper mappingMapper;
@Override
public List<BmsProductCodeMapping> selectList(BmsProductCodeMapping query)
{
return mappingMapper.selectList(query);
}
@Override
public BmsProductCodeMapping selectById(Long mapId)
{
return mappingMapper.selectById(mapId);
}
@Override
public int insert(BmsProductCodeMapping row)
{
normalize(row);
validate(row);
if (mappingMapper.countByUpstreamCode(row.getUpstreamCode(), null) > 0)
{
throw new ServiceException("上游产品代码已存在映射:" + row.getUpstreamCode());
}
if (StringUtils.isEmpty(row.getStatus()))
{
row.setStatus("0");
}
return mappingMapper.insert(row);
}
@Override
public int update(BmsProductCodeMapping row)
{
if (row.getMapId() == null)
{
throw new ServiceException("mapId required");
}
normalize(row);
validate(row);
if (mappingMapper.countByUpstreamCode(row.getUpstreamCode(), row.getMapId()) > 0)
{
throw new ServiceException("上游产品代码已存在映射:" + row.getUpstreamCode());
}
return mappingMapper.update(row);
}
@Override
public int deleteByIds(Long[] mapIds)
{
return mappingMapper.deleteByIds(mapIds);
}
@Override
public String resolveBmsProductCode(String upstreamCode)
{
if (StringUtils.isEmpty(upstreamCode))
{
return upstreamCode;
}
String code = upstreamCode.trim();
BmsProductCodeMapping mapped = mappingMapper.selectActiveByUpstreamCode(code);
if (mapped != null && StringUtils.isNotEmpty(mapped.getBmsProductCode()))
{
return mapped.getBmsProductCode().trim();
}
return code;
}
private void normalize(BmsProductCodeMapping row)
{
if (row.getUpstreamCode() != null)
{
row.setUpstreamCode(row.getUpstreamCode().trim());
}
if (row.getBmsProductCode() != null)
{
row.setBmsProductCode(row.getBmsProductCode().trim());
}
}
private void validate(BmsProductCodeMapping row)
{
if (StringUtils.isEmpty(row.getUpstreamCode()))
{
throw new ServiceException("上游产品代码不能为空");
}
if (StringUtils.isEmpty(row.getBmsProductCode()))
{
throw new ServiceException("BMS产品代码不能为空");
}
}
}
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bms.system.mapper.BmsProductCodeMappingMapper">
<resultMap type="BmsProductCodeMapping" id="BmsProductCodeMappingResult">
<id property="mapId" column="map_id" />
<result property="upstreamCode" column="upstream_code" />
<result property="bmsProductCode" column="bms_product_code" />
<result property="status" column="status" />
<result property="remark" column="remark" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="delFlag" column="del_flag" />
</resultMap>
<sql id="selectVo">
select map_id, upstream_code, bms_product_code, status, remark,
create_by, create_time, update_by, update_time, del_flag
from product_code_mapping
</sql>
<select id="selectList" parameterType="BmsProductCodeMapping" resultMap="BmsProductCodeMappingResult">
<include refid="selectVo"/>
<where>
del_flag = '0'
<if test="upstreamCode != null and upstreamCode != ''">
and upstream_code like concat('%', #{upstreamCode}, '%')
</if>
<if test="bmsProductCode != null and bmsProductCode != ''">
and bms_product_code like concat('%', #{bmsProductCode}, '%')
</if>
<if test="status != null and status != ''">
and status = #{status}
</if>
</where>
order by map_id desc
</select>
<select id="selectById" parameterType="Long" resultMap="BmsProductCodeMappingResult">
<include refid="selectVo"/>
where map_id = #{mapId} and del_flag = '0'
</select>
<select id="selectActiveByUpstreamCode" resultMap="BmsProductCodeMappingResult">
<include refid="selectVo"/>
where del_flag = '0' and status = '0' and upstream_code = #{upstreamCode}
order by map_id desc
limit 1
</select>
<select id="countByUpstreamCode" resultType="int">
select count(1) from product_code_mapping
where del_flag = '0' and upstream_code = #{upstreamCode}
<if test="excludeId != null">and map_id != #{excludeId}</if>
</select>
<insert id="insert" parameterType="BmsProductCodeMapping" useGeneratedKeys="true" keyProperty="mapId">
insert into product_code_mapping (
upstream_code, bms_product_code, status, remark, create_by, create_time, del_flag
) values (
#{upstreamCode}, #{bmsProductCode}, #{status}, #{remark}, #{createBy}, sysdate(), '0'
)
</insert>
<update id="update" parameterType="BmsProductCodeMapping">
update product_code_mapping
<set>
<if test="upstreamCode != null and upstreamCode != ''">upstream_code = #{upstreamCode},</if>
<if test="bmsProductCode != null and bmsProductCode != ''">bms_product_code = #{bmsProductCode},</if>
<if test="status != null and status != ''">status = #{status},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
update_time = sysdate()
</set>
where map_id = #{mapId}
</update>
<update id="deleteByIds" parameterType="Long">
update product_code_mapping
set del_flag = '1',
upstream_code = concat(upstream_code, '#DEL#', map_id),
update_time = sysdate()
where map_id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</update>
</mapper>
@@ -107,4 +107,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
WHERE rn &gt; #{startRow}
</select>
<!-- Distinct product codes under the same filters (no pagination). -->
<select id="selectTabOrderProductCodes" resultType="java.util.HashMap">
SELECT o.CUSTOMER_CODE AS CUSTOMER_CODE,
o.PRODUCT_CODE AS PRODUCT_CODE,
MAX(o.PRODUCT_NAME) AS PRODUCT_NAME,
COUNT(1) AS ORDER_COUNT
FROM ${orderTable} o
WHERE
<include refid="tabOrderMonitorWhere"/>
AND o.PRODUCT_CODE IS NOT NULL
GROUP BY o.CUSTOMER_CODE, o.PRODUCT_CODE
ORDER BY o.CUSTOMER_CODE, o.PRODUCT_CODE
</select>
</mapper>
@@ -0,0 +1,18 @@
-- 产品代码映射(上游代码 -> BMS产品代码)
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS product_code_mapping (
map_id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '映射ID',
upstream_code varchar(64) NOT NULL COMMENT '上游产品代码',
bms_product_code varchar(64) NOT NULL COMMENT 'BMS产品代码',
status char(1) NOT NULL DEFAULT '0' COMMENT '状态(0正常 1停用)',
remark varchar(500) DEFAULT NULL COMMENT '备注',
create_by varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime DEFAULT NULL COMMENT '创建时间',
update_by varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime DEFAULT NULL COMMENT '更新时间',
del_flag char(1) DEFAULT '0' COMMENT '删除标记(0存在 1删除)',
PRIMARY KEY (map_id),
UNIQUE KEY uk_upstream_code (upstream_code),
KEY idx_bms_product_code (bms_product_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品代码映射表';
@@ -0,0 +1,37 @@
-- 数据管理 - 代码映射
SET NAMES utf8mb4;
SET @parentId := (
SELECT menu_id FROM sys_menu
WHERE path = 'data' AND menu_type = 'M'
ORDER BY menu_id ASC LIMIT 1
);
SET @parentId := IFNULL(@parentId, (
SELECT parent_id FROM sys_menu
WHERE perms = 'data:region:list' OR component = 'data/region/index' OR component = 'data/exchangerate/index'
ORDER BY menu_id ASC LIMIT 1
));
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT 2468, N'代码映射', @parentId, 30, 'productCodeMap', 'data/productCodeMap/index', '', 'ProductCodeMap', 1, 0, 'C', '0', '0', 'data:productCodeMap:list', 'tree-table', 'admin', sysdate(), N'上游产品代码与BMS产品代码映射,计费匹配用'
FROM DUAL WHERE @parentId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2468 OR perms = 'data:productCodeMap:list');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT 2469, N'查询', 2468, 1, '', NULL, 1, 0, 'F', '0', '0', 'data:productCodeMap:query', '#', 'admin', sysdate()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2469);
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT 2470, N'新增', 2468, 2, '', NULL, 1, 0, 'F', '0', '0', 'data:productCodeMap:add', '#', 'admin', sysdate()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2470);
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT 2471, N'修改', 2468, 3, '', NULL, 1, 0, 'F', '0', '0', 'data:productCodeMap:edit', '#', 'admin', sysdate()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2471);
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT 2472, N'删除', 2468, 4, '', NULL, 1, 0, 'F', '0', '0', 'data:productCodeMap:remove', '#', 'admin', sysdate()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2472);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, m.menu_id FROM (SELECT 2468 AS menu_id UNION SELECT 2469 UNION SELECT 2470 UNION SELECT 2471 UNION SELECT 2472) m
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
+39
View File
@@ -0,0 +1,39 @@
import request from '@/utils/request'
export function listProductCodeMap(query) {
return request({
url: '/data/productCodeMap/list',
method: 'get',
params: query
})
}
export function getProductCodeMap(mapId) {
return request({
url: '/data/productCodeMap/' + mapId,
method: 'get'
})
}
export function addProductCodeMap(data) {
return request({
url: '/data/productCodeMap',
method: 'post',
data
})
}
export function updateProductCodeMap(data) {
return request({
url: '/data/productCodeMap',
method: 'put',
data
})
}
export function delProductCodeMap(mapIds) {
return request({
url: '/data/productCodeMap/' + mapIds,
method: 'delete'
})
}
+9
View File
@@ -48,3 +48,12 @@ export function getOrderSourceSyncLog(docId) {
method: 'get'
})
}
export function checkOrderSourceProducts(data) {
return request({
url: '/monitor/orderSource/checkProducts',
method: 'post',
data,
timeout: 180000
})
}
@@ -0,0 +1,177 @@
<template>
<div class="app-container">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
row-key="mapId"
selection
>
<template #toolbar="{ ids }">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['data:productCodeMap:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="!ids.length" @click="handleDelete(ids)" v-hasPermi="['data:productCodeMap:remove']">删除</el-button>
</el-col>
</template>
<template #col-status="{ row }">
<el-tag size="small" :type="row.status === '0' ? 'success' : 'info'">{{ row.status === '0' ? '正常' : '停用' }}</el-tag>
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['data:productCodeMap:edit']">修改</el-button>
<el-button link type="danger" icon="Delete" @click="handleDelete([row.mapId])" v-hasPermi="['data:productCodeMap:remove']">删除</el-button>
</template>
</basic-table>
<el-dialog :title="title" v-model="open" width="520px" append-to-body destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="上游产品代码" prop="upstreamCode">
<el-input v-model="form.upstreamCode" placeholder="上游/渠道产品代码" maxlength="64" />
</el-form-item>
<el-form-item label="BMS产品代码" prop="bmsProductCode">
<el-select
v-model="form.bmsProductCode"
filterable
remote
clearable
reserve-keyword
placeholder="请输入产品代码或名称"
: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>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio value="0">正常</el-radio>
<el-radio value="1">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="submitForm">确定</el-button>
<el-button @click="open = false">取消</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="ProductCodeMap">
import { listProductCodeMap, getProductCodeMap, addProductCodeMap, updateProductCodeMap, delProductCodeMap } from '@/api/data/productCodeMap'
import { listProductOptions } from '@/api/price/product'
const { proxy } = getCurrentInstance()
const tableRef = ref()
const formRef = ref()
const open = ref(false)
const title = ref('')
const productLoading = ref(false)
const productOptions = ref([])
const statusOptions = [
{ label: '正常', value: '0' },
{ label: '停用', value: '1' }
]
const searchColumns = [
{ label: '上游代码', prop: 'upstreamCode', component: 'input', placeholder: '上游产品代码', width: '180px' },
{ label: 'BMS代码', prop: 'bmsProductCode', component: 'input', placeholder: 'BMS产品代码', width: '180px' },
{ label: '状态', prop: 'status', component: 'select', options: statusOptions, placeholder: '请选择', width: '140px', props: { clearable: true } }
]
const columns = [
{ label: '上游产品代码', prop: 'upstreamCode', minWidth: 160, showOverflowTooltip: true },
{ label: 'BMS产品代码', prop: 'bmsProductCode', minWidth: 160, showOverflowTooltip: true },
{ label: '状态', prop: 'status', align: 'center', width: 90, slot: 'status' },
{ label: '备注', prop: 'remark', minWidth: 160, showOverflowTooltip: true },
{ label: '创建时间', prop: 'createTime', align: 'center', width: 170 },
{ label: '操作', prop: 'action', align: 'center', width: 150, fixed: 'right', slot: 'action', showInToolbar: false }
]
const data = reactive({
form: {},
rules: {
upstreamCode: [{ required: true, message: '请输入上游产品代码', trigger: 'blur' }],
bmsProductCode: [{ required: true, message: '请选择BMS产品代码', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
})
const { form, rules } = toRefs(data)
function queryList(params) {
return listProductCodeMap(params)
}
function resetForm() {
form.value = {
mapId: undefined,
upstreamCode: '',
bmsProductCode: '',
status: '0',
remark: ''
}
productOptions.value = []
}
function handleAdd() {
resetForm()
title.value = '新增代码映射'
open.value = true
}
function handleUpdate(row) {
resetForm()
getProductCodeMap(row.mapId).then(res => {
form.value = Object.assign({}, res.data || {})
if (form.value.bmsProductCode) {
productOptions.value = [{ productCode: form.value.bmsProductCode, productName: form.value.bmsProductCode }]
searchProduct(form.value.bmsProductCode)
}
title.value = '修改代码映射'
open.value = true
})
}
function submitForm() {
formRef.value.validate(valid => {
if (!valid) return
const req = form.value.mapId ? updateProductCodeMap(form.value) : addProductCodeMap(form.value)
req.then(() => {
proxy.$modal.msgSuccess('保存成功')
open.value = false
tableRef.value?.refresh()
})
})
}
function handleDelete(ids) {
if (!ids || !ids.length) return
proxy.$modal.confirm('确认删除选中的 ' + ids.length + ' 条映射?').then(() => {
return delProductCodeMap(ids.join(','))
}).then(() => {
proxy.$modal.msgSuccess('删除成功')
tableRef.value?.refresh()
}).catch(() => {})
}
function searchProduct(keyword) {
productLoading.value = true
listProductOptions({ productCode: keyword || undefined, status: '1' }).then(res => {
productOptions.value = res.data || res.rows || []
}).finally(() => {
productLoading.value = false
})
}
</script>
@@ -0,0 +1,132 @@
<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-alert
v-if="result.truncated"
type="warning"
:closable="false"
show-icon
class="mb12"
title="订单超过上限,仅对已扫描部分进行比对"
/>
<el-descriptions :column="3" border size="small" class="mb12">
<el-descriptions-item label="筛选订单数">{{ result.orderTotal ?? 0 }}</el-descriptions-item>
<el-descriptions-item label="已扫描">{{ result.scannedOrders ?? 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="订单产品数">{{ result.distinctOrderProducts ?? 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="upstreamCode" label="上游产品代码" min-width="120" show-overflow-tooltip />
<el-table-column prop="productName" label="产品名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="mappedCode" label="映射后代码" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ row.mappedCode || '-' }}</template>
</el-table-column>
<el-table-column prop="orderCount" label="订单数" width="90" align="center" />
<el-table-column label="示例订单" min-width="180" show-overflow-tooltip>
<template #default="{ row }">{{ (row.sampleOrderIds || []).join(', ') || '-' }}</template>
</el-table-column>
</el-table>
<el-empty v-if="!(result.missingProducts || []).length" description="未发现缺失产品" />
</el-tab-pane>
<el-tab-pane label="全部比对" name="all">
<el-table :data="result.orderProducts || []" border size="small" max-height="520">
<el-table-column prop="customerCode" label="客户代码" min-width="110" show-overflow-tooltip />
<el-table-column prop="upstreamCode" label="上游产品代码" min-width="120" show-overflow-tooltip />
<el-table-column prop="productName" label="产品名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="mappedCode" label="映射后代码" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ row.mappedCode || '-' }}</template>
</el-table-column>
<el-table-column label="是否在套餐" width="120" align="center">
<template #default="{ row }">
<el-tag v-if="row.covered" size="small" type="success">在套餐</el-tag>
<el-tag v-else size="small" type="danger">不在套餐</el-tag>
</template>
</el-table-column>
<el-table-column label="命中方式" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ hitText(row) }}</template>
</el-table-column>
<el-table-column prop="orderCount" label="订单数" width="90" align="center" />
</el-table>
</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="160" show-overflow-tooltip />
</el-table>
<el-empty v-if="!(result.packageProducts || []).length" description="客户未绑定套餐产品" />
</el-tab-pane>
</el-tabs>
</template>
</div>
</el-drawer>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { checkOrderSourceProducts } from '@/api/monitor/orderSource'
const visible = ref(false)
const loading = ref(false)
const result = ref(null)
const activeTab = ref('missing')
const missingTabLabel = computed(() => {
const n = result.value?.missingCount ?? 0
return '缺失产品 (' + n + ')'
})
const packageTabLabel = computed(() => {
const n = result.value?.packageProductCount ?? 0
return '套餐产品 (' + n + ')'
})
function hitText(row) {
if (!row) return '-'
if (row.inPackage) return '直接命中'
if (row.mappedInPackage) return '映射后命中'
if (row.mappedCode) return '有映射但仍不在套餐'
return '无映射'
}
function open(query) {
if (!query || !query.customerCodes || !query.beginTime || !query.endTime) {
ElMessage.warning('请先选择客户和提货时间')
return
}
visible.value = true
activeTab.value = 'missing'
result.value = null
loading.value = true
checkOrderSourceProducts(query).then(res => {
result.value = res.data || null
if ((result.value?.missingCount || 0) > 0) {
activeTab.value = 'missing'
} else {
activeTab.value = 'all'
}
}).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>
@@ -46,6 +46,16 @@
@click="openFilterSync()"
>同步计费</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Search"
:loading="checkLoading"
v-hasPermi="['monitor:orderSource:list']"
@click="handleCheckProducts()"
>检测产品</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
@@ -93,6 +103,7 @@
@success="onSyncSuccess"
/>
<order-source-sync-log-drawer ref="logDrawerRef" />
<order-source-product-check-drawer ref="productCheckDrawerRef" />
</div>
</template>
@@ -101,11 +112,14 @@ import { ElMessage } from 'element-plus'
import { listOrderSource, syncOrderSourceSelected } from '@/api/monitor/orderSource'
import OrderSourceSyncDialog from './components/OrderSourceSyncDialog.vue'
import OrderSourceSyncLogDrawer from './components/OrderSourceSyncLogDrawer.vue'
import OrderSourceProductCheckDrawer from './components/OrderSourceProductCheckDrawer.vue'
const { proxy } = getCurrentInstance()
const tableRef = ref()
const syncDialogRef = ref()
const logDrawerRef = ref()
const productCheckDrawerRef = ref()
const checkLoading = ref(false)
const syncingKey = ref(null)
const batchLoading = ref(false)
const syncFloat = ref(null)
@@ -281,6 +295,24 @@ function handleSyncOne(row) {
})
}
function handleCheckProducts() {
const q = buildFilterQuery()
if (!q.customerCodes || !q.beginTime || !q.endTime) {
ElMessage.warning('请先选择客户和提货时间')
return
}
const codes = Array.isArray(q.customerCodes) ? q.customerCodes.filter(v => v != null && String(v).trim() !== '') : []
if (!codes.length) {
ElMessage.warning('请先选择客户和提货时间')
return
}
q.customerCodes = codes
checkLoading.value = true
productCheckDrawerRef.value?.open(q)
window.setTimeout(() => { checkLoading.value = false }, 300)
}
function handleBatchSync(selection) {
if (!selection || !selection.length) return
proxy.$modal.confirm('确认同步选中的 ' + selection.length + ' 条订单?', '二次确认').then(() => {