Merge branch 'test' into feauture-20261104
# Conflicts: # bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcProfitReportServiceImpl.java
This commit is contained in:
@@ -1733,7 +1733,7 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
case CATALOG_COUNTRY -> "产品价未匹配国家";
|
||||
case CATALOG_WEIGHT -> "产品价未匹配重量段";
|
||||
case NO_CUSTOMER_PACKAGE -> "未绑定套餐";
|
||||
case NO_ACTIVE_PACKAGE_VERSION -> "无生效套餐版本";
|
||||
case NO_ACTIVE_PACKAGE_VERSION -> "套餐未启用/无生效版本";
|
||||
case ALREADY_IN_AR_BILL -> "已生成应收账单";
|
||||
case PRODUCT_ARCHIVE_MISSING -> "产品档案缺失";
|
||||
case PRICE_NOT_HIT -> "未命中价格";
|
||||
|
||||
+3
-3
@@ -47,9 +47,9 @@ public class BmsCustomer extends BaseEntity
|
||||
@Excel(name = "业务类型", readConverterExp = "0=直客,1=同行,2=集运")
|
||||
private String customerType;
|
||||
|
||||
/** 区域类型(字典 data_area_type) */
|
||||
@Excel(name = "区域类型", dictType = "data_area_type")
|
||||
private String areaType;
|
||||
/** 客户区域(字典 customer_area) */
|
||||
@Excel(name = "客户区域", dictType = "customer_area")
|
||||
private String customerArea;
|
||||
|
||||
/** 启用状态(0正常 1停用) */
|
||||
@Excel(name = "启用状态", readConverterExp = "0=正常,1=停用")
|
||||
|
||||
+7
-7
@@ -45,10 +45,10 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
|
||||
private static final ThreadLocal<SimpleDateFormat> DATE_FMT =
|
||||
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
|
||||
|
||||
/** 区域类型走字典 data_area_type;缓存未命中时保留原编码。 */
|
||||
private static String areaTypeLabel(String value)
|
||||
/** 客户区域走字典 customer_area;缓存未命中时保留原编码。 */
|
||||
private static String customerAreaLabel(String value)
|
||||
{
|
||||
String label = DictUtils.getDictLabel("data_area_type", value);
|
||||
String label = DictUtils.getDictLabel("customer_area", value);
|
||||
return StringUtils.isEmpty(label) ? value : label;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
|
||||
/**
|
||||
* 需要记录日志的主表字段(顺序即展示顺序)。
|
||||
* requireNonBlank 为 true 的字段对应 update SQL 的 <if test="x!=null and x!=''">,
|
||||
* 其余动态字段仅在新值非 null 时落库;业务员/结算员/客服、区域类型为无条件写入。
|
||||
* 其余动态字段仅在新值非 null 时落库;业务员/结算员/客服、客户区域为无条件写入。
|
||||
*/
|
||||
private static final List<FieldDef> FIELD_DEFS = List.of(
|
||||
new FieldDef("customerCode", "客户编码", BmsCustomer::getCustomerCode, true, Function.identity()),
|
||||
@@ -85,7 +85,7 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
|
||||
new FieldDef("customerCodeDw", "对外代码", BmsCustomer::getCustomerCodeDw, false, Function.identity()),
|
||||
new FieldDef("creditCode", "统一社会信用代码", BmsCustomer::getCreditCode, false, Function.identity()),
|
||||
new FieldDef("customerType", "业务类型", BmsCustomer::getCustomerType, false, CUSTOMER_TYPE_MAP::get),
|
||||
new FieldDef("areaType", "区域类型", BmsCustomer::getAreaType, false, BmsCustomerLogServiceImpl::areaTypeLabel),
|
||||
new FieldDef("customerArea", "客户区域", BmsCustomer::getCustomerArea, false, BmsCustomerLogServiceImpl::customerAreaLabel),
|
||||
new FieldDef("isEnable", "启用状态", BmsCustomer::getIsEnable, false, ENABLE_MAP::get),
|
||||
new FieldDef("salesUserId", "业务员", c -> userDisplay(c.getSalesUserId(), c.getSalesUser()), false, Function.identity()),
|
||||
new FieldDef("ed", "资金额度(元)", BmsCustomer::getEd, false, BmsCustomerLogServiceImpl::numText),
|
||||
@@ -208,8 +208,8 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
|
||||
{
|
||||
// 与 update SQL 的动态条件保持一致:未提交(null/空串)的字段本次不落库,不参与比对
|
||||
Object raw = def.rawGetter.apply(newCustomer);
|
||||
// 合同起止、区域类型允许清空;其余动态字段未提交时不落库,也不记日志
|
||||
if (raw == null && !isContractDateField(def.field) && !"areaType".equals(def.field))
|
||||
// 合同起止、客户区域允许清空;其余动态字段未提交时不落库,也不记日志
|
||||
if (raw == null && !isContractDateField(def.field) && !"customerArea".equals(def.field))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
+2
@@ -60,6 +60,7 @@ public class CustomerSyncService
|
||||
private static final int LEN_CODE = 50;
|
||||
private static final int LEN_NAME = 100;
|
||||
private static final int LEN_CODE_DW = 50;
|
||||
private static final int LEN_CUSTOMER_AREA = 32;
|
||||
private static final int LEN_USER = 50;
|
||||
private static final int LEN_REMARK = 500;
|
||||
/** 告警明细最多打印条数 */
|
||||
@@ -178,6 +179,7 @@ public class CustomerSyncService
|
||||
c.setDelFlag("0");
|
||||
|
||||
c.setCustomerCodeDw(truncate(getStr(row, "CUSTOMER_OUTER_CODE"), LEN_CODE_DW, code, "customer_code_dw", warnings));
|
||||
c.setCustomerArea(truncate(getStr(row, "CUSTOMER_AREA"), LEN_CUSTOMER_AREA, code, "customer_area", warnings));
|
||||
String sourceSalesCode = clean(getStr(row, "BELONG_SALER"));
|
||||
c.setSourceSalesCode(sourceSalesCode);
|
||||
c.setSalesUserId(resolveSalesUserId(sourceSalesCode, userIdsByLogin,
|
||||
|
||||
@@ -11,6 +11,7 @@ public class BatchAddIncomeBillFeeTask implements Serializable
|
||||
public static final String STATUS_RUNNING = "RUNNING";
|
||||
public static final String STATUS_SUCCESS = "SUCCESS";
|
||||
public static final String STATUS_FAIL = "FAIL";
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
|
||||
private String taskId;
|
||||
private String status;
|
||||
|
||||
@@ -15,12 +15,19 @@ public class PrcPriceSet extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** Ordinary package (default). */
|
||||
public static final String CATEGORY_PACKAGE = "PACKAGE";
|
||||
/** Boss exclusive quote. */
|
||||
public static final String CATEGORY_EXCLUSIVE = "EXCLUSIVE";
|
||||
|
||||
private Long priceSetId;
|
||||
private String priceSetCode;
|
||||
private String priceSetName;
|
||||
private String priceSetNote;
|
||||
/** FIXED / DISCOUNT */
|
||||
private String setType;
|
||||
/** PACKAGE=ordinary set / EXCLUSIVE=boss exclusive quote */
|
||||
private String setCategory;
|
||||
private BigDecimal defaultDiscount;
|
||||
private String defaultType;
|
||||
private String calculateSign;
|
||||
|
||||
@@ -24,6 +24,10 @@ public class PrcProfitReportQuery extends BaseEntity
|
||||
private String customerCode;
|
||||
/** 客户代码拆分后的多值查询条件。 */
|
||||
private List<String> customerCodes;
|
||||
/** 按客户当前区域筛选。 */
|
||||
private String customerArea;
|
||||
/** 服务端根据客户区域确定的计算规则。 */
|
||||
private String profitRule;
|
||||
private String customerName;
|
||||
/** 客户名称拆分后的多值查询条件。 */
|
||||
private List<String> customerNames;
|
||||
|
||||
@@ -43,6 +43,11 @@ public class TabOrderMonitorQuery implements Serializable
|
||||
* 空则服务端回落默认 3,4,5,6,7,8,11(与历史硬编码一致)。
|
||||
*/
|
||||
private List<Integer> orderStatuses;
|
||||
/**
|
||||
* 是否收货 BL_PICKUP。1 已收货,0 未收货。
|
||||
* 空则服务端回落 1,与历史查询一致。
|
||||
*/
|
||||
private Integer blPickup;
|
||||
/** 分批同步计费每批条数 */
|
||||
private Integer batchSize;
|
||||
/** Excel export field names (Java props; export only). */
|
||||
|
||||
@@ -66,7 +66,7 @@ public enum BillTaskFailCode
|
||||
{
|
||||
return NO_CUSTOMER_PACKAGE.code;
|
||||
}
|
||||
if (containsAny(msg, "无生效套餐版本", "noActivePackageVersion", "NO_ACTIVE_PACKAGE_VERSION"))
|
||||
if (containsAny(msg, "无生效套餐版本", "套餐未启用", "noActivePackageVersion", "NO_ACTIVE_PACKAGE_VERSION", "packageNotEnabled"))
|
||||
{
|
||||
return NO_ACTIVE_PACKAGE_VERSION.code;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.bms.price.enums;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/** 客户区域字典值对应的毛利计算规则。 */
|
||||
public enum ProfitAreaRule
|
||||
{
|
||||
SHENZHEN("C"),
|
||||
GUANGZHOU("GZ");
|
||||
|
||||
private final String customerArea;
|
||||
|
||||
ProfitAreaRule(String customerArea)
|
||||
{
|
||||
this.customerArea = customerArea;
|
||||
}
|
||||
|
||||
public static ProfitAreaRule fromCustomerArea(String customerArea)
|
||||
{
|
||||
if (customerArea != null)
|
||||
{
|
||||
for (ProfitAreaRule rule : values())
|
||||
{
|
||||
if (rule.customerArea.equals(customerArea.trim()))
|
||||
{
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Map<String, String> areaRules()
|
||||
{
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for (ProfitAreaRule rule : values())
|
||||
{
|
||||
result.put(rule.customerArea, rule.name());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -70,7 +70,8 @@ public interface OracleUpstreamOrderMapper
|
||||
List<Map<String, Object>> selectMatchedTransferOrderKeys(
|
||||
@Param("orderTable") String orderTable,
|
||||
@Param("batch") List<String> batch,
|
||||
@Param("customerCodes") List<String> customerCodes);
|
||||
@Param("customerCodes") List<String> customerCodes,
|
||||
@Param("blPickup") Integer blPickup);
|
||||
|
||||
|
||||
/** 按已收集到的订单主键批量查明细(一批为一个内存分页切片)。 */
|
||||
|
||||
@@ -82,6 +82,18 @@ public interface PrcPriceSetDetailMapper
|
||||
@Param("countryName") String countryName,
|
||||
@Param("zoneCode") String zoneCode);
|
||||
|
||||
/**
|
||||
* Next FIXED weight band whose weight_min is strictly above {@code weight}
|
||||
* (ceil into the gap between bands, e.g. 1.001 -> 1.01~2).
|
||||
*/
|
||||
public PrcPriceSetDetail selectFixedNextBandUp(
|
||||
@Param("psvId") Long psvId,
|
||||
@Param("productCode") String productCode,
|
||||
@Param("countryCode") String countryCode,
|
||||
@Param("countryName") String countryName,
|
||||
@Param("zoneCode") String zoneCode,
|
||||
@Param("weight") BigDecimal weight);
|
||||
|
||||
public int countZonedByPsvProductCountry(
|
||||
@Param("psvId") Long psvId,
|
||||
@Param("productCode") String productCode,
|
||||
|
||||
+3
-2
@@ -28,8 +28,9 @@ public class OracleOrderKeyCollectWorker
|
||||
|
||||
/** 比对回填转单号专用分批(不走三列 UNION)。 */
|
||||
@DataSource(DataSourceType.SLAVE)
|
||||
public List<Map<String, Object>> collectTransferBatch(String table, List<String> batch, List<String> customerCodes)
|
||||
public List<Map<String, Object>> collectTransferBatch(String table, List<String> batch, List<String> customerCodes,
|
||||
Integer blPickup)
|
||||
{
|
||||
return oracleUpstreamOrderMapper.selectMatchedTransferOrderKeys(table, batch, customerCodes);
|
||||
return oracleUpstreamOrderMapper.selectMatchedTransferOrderKeys(table, batch, customerCodes, blPickup);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,12 +456,13 @@ public class TabOrderMonitorService
|
||||
}
|
||||
}
|
||||
List<String> customerCodes = cleanedCodes.isEmpty() ? null : cleanedCodes;
|
||||
normalizeBlPickup(query);
|
||||
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<MatchedOrderKey> keys = doCollectTransferKeys(table, nos, customerCodes, query.getBlPickup());
|
||||
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()),
|
||||
@@ -795,12 +796,14 @@ public class TabOrderMonitorService
|
||||
* 回填转单号专用收集:只走 selectMatchedTransferOrderKeys,
|
||||
* 不走三列 UNION、不带提货时间。
|
||||
*/
|
||||
private List<MatchedOrderKey> doCollectTransferKeys(String table, List<String> transferNos, List<String> customerCodes)
|
||||
private List<MatchedOrderKey> doCollectTransferKeys(String table, List<String> transferNos, List<String> customerCodes,
|
||||
Integer blPickup)
|
||||
{
|
||||
StringBuilder fp = new StringBuilder("tf|").append(table == null ? "" : table).append('|');
|
||||
appendSorted(fp, transferNos);
|
||||
fp.append('|');
|
||||
appendSorted(fp, customerCodes);
|
||||
fp.append('|').append(blPickup == null ? "" : blPickup);
|
||||
String fingerprint = Integer.toHexString(fp.toString().hashCode());
|
||||
long now = System.currentTimeMillis();
|
||||
CacheEntry cached = collectCache.get(fingerprint);
|
||||
@@ -821,7 +824,7 @@ public class TabOrderMonitorService
|
||||
new ArrayList<Future<List<Map<String, Object>>>>(batches.size());
|
||||
for (List<String> batch : batches)
|
||||
{
|
||||
futures.add(collectPool.submit(() -> collectWorker.collectTransferBatch(table, batch, customerCodes)));
|
||||
futures.add(collectPool.submit(() -> collectWorker.collectTransferBatch(table, batch, customerCodes, blPickup)));
|
||||
}
|
||||
|
||||
Map<String, Timestamp> unique = new LinkedHashMap<String, Timestamp>();
|
||||
@@ -1035,6 +1038,7 @@ public class TabOrderMonitorService
|
||||
.append('|').append(safe(q.getOrderId())).append('|').append(safe(q.getOrgOrderId()))
|
||||
.append('|').append(safe(q.getTransferOrderId())).append('|');
|
||||
appendSortedInts(sb, q.getOrderStatuses());
|
||||
sb.append('|').append(q.getBlPickup() == null ? "" : q.getBlPickup());
|
||||
return Integer.toHexString(sb.toString().hashCode());
|
||||
}
|
||||
|
||||
@@ -1207,6 +1211,7 @@ public class TabOrderMonitorService
|
||||
query.setOrgOrderId(trimToNull(query.getOrgOrderId()));
|
||||
query.setTransferOrderId(trimToNull(query.getTransferOrderId()));
|
||||
normalizeOrderStatuses(query);
|
||||
normalizeBlPickup(query);
|
||||
}
|
||||
|
||||
/** Empty/missing -> default 3,4,5,6,7,8,11 so SQL always has a status IN list. */
|
||||
@@ -1227,6 +1232,16 @@ public class TabOrderMonitorService
|
||||
query.setOrderStatuses(cleaned.isEmpty() ? new ArrayList<Integer>(DEFAULT_ORDER_STATUSES) : cleaned);
|
||||
}
|
||||
|
||||
/** Empty or anything other than 0/1 falls back to 1(已收货). */
|
||||
private static void normalizeBlPickup(TabOrderMonitorQuery query)
|
||||
{
|
||||
Integer blPickup = query.getBlPickup();
|
||||
if (blPickup == null || (blPickup.intValue() != 0 && blPickup.intValue() != 1))
|
||||
{
|
||||
query.setBlPickup(Integer.valueOf(1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多行文本归一化:按换行/逗号/分号/顿号拆分(不按空格拆),trim、去空去重。
|
||||
*/
|
||||
@@ -1309,8 +1324,9 @@ public class TabOrderMonitorService
|
||||
vo.setTrackId(str(row, "TRACK_ID"));
|
||||
vo.setCustomerCode(str(row, "CUSTOMER_CODE"));
|
||||
vo.setCustomerName(str(row, "CUSTOMER_NAME"));
|
||||
vo.setProductCode(str(row, "PRODUCT_CODE"));
|
||||
vo.setProductName(str(row, "PRODUCT_NAME"));
|
||||
// Prefer LINE_CODE / LINE_NAME (billing product); list SQL aliases them as PRODUCT_*.
|
||||
vo.setProductCode(firstStr(row, "LINE_CODE", "PRODUCT_CODE"));
|
||||
vo.setProductName(firstStr(row, "LINE_NAME", "PRODUCT_NAME"));
|
||||
vo.setProductFloorCode(str(row, "PRODUCT_FLOOR_CODE"));
|
||||
vo.setCountry(str(row, "COUNTRY"));
|
||||
vo.setCountryName(str(row, "COUNTRY_NAME"));
|
||||
@@ -1330,6 +1346,23 @@ public class TabOrderMonitorService
|
||||
return v == null ? null : String.valueOf(v).trim();
|
||||
}
|
||||
|
||||
private static String firstStr(Map<String, Object> row, String... keys)
|
||||
{
|
||||
if (keys == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
for (String key : keys)
|
||||
{
|
||||
String v = str(row, key);
|
||||
if (StringUtils.isNotEmpty(v))
|
||||
{
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Integer integer(Map<String, Object> row, String key)
|
||||
{
|
||||
Object v = value(row, key);
|
||||
|
||||
@@ -59,10 +59,6 @@ public interface IBmsProductService
|
||||
|
||||
public java.util.Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(byte[] bytes, String filename) throws Exception;
|
||||
|
||||
/**
|
||||
* Parse single-country dedicated-line quote Excel (same format as product country import).
|
||||
* All weight rows are stamped with the selected country. Keys are upper-case product codes.
|
||||
*/
|
||||
public java.util.Map<String, List<BmsProductCountry>> parseCountryQuoteExcelByProduct(byte[] bytes, String filename,
|
||||
String countryCode) throws Exception;
|
||||
|
||||
|
||||
@@ -166,6 +166,9 @@ public interface IPrcOrderBillService
|
||||
/** Poll Redis progress for local rebill task. */
|
||||
BatchAddIncomeBillFeeTask getLocalRebillTask(String taskId);
|
||||
|
||||
/** Cooperative cancel for a running local rebill task. */
|
||||
BatchAddIncomeBillFeeTask cancelLocalRebill(String taskId);
|
||||
|
||||
/** Data-scoped bill_id list for local rebill (same filters as order-bill list). */
|
||||
List<String> selectBillIdsForLocalRebill(PrcOrderBill query);
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ public interface IPrcPriceSetService
|
||||
|
||||
public List<PrcPriceSet> selectPrcPriceSetList(PrcPriceSet set);
|
||||
|
||||
/** Fail if the package is not in the expected category (PACKAGE / EXCLUSIVE). */
|
||||
public void assertSetCategory(Long priceSetId, String expectedCategory);
|
||||
|
||||
public Long savePrcPriceSet(PrcPriceSet set);
|
||||
|
||||
public int updatePrcPriceSet(PrcPriceSet set);
|
||||
@@ -149,6 +152,13 @@ public interface IPrcPriceSetService
|
||||
boolean updateOnly, List<PrcPriceSetDetail> existingDetails, boolean replaceByProductCountry, String countryCode)
|
||||
throws Exception;
|
||||
|
||||
/**
|
||||
* @param exclusiveTemplate true = HALARA large-customer exclusive quote Excel (destination / handling fee)
|
||||
*/
|
||||
public Map<String, Object> previewImportDetailPricesMany(List<byte[]> bytesList, List<String> names,
|
||||
boolean updateOnly, List<PrcPriceSetDetail> existingDetails, boolean replaceByProductCountry, String countryCode,
|
||||
boolean exclusiveTemplate) throws Exception;
|
||||
|
||||
/**
|
||||
* Sync worker: parse bytes and upsert FIXED detail prices matched by
|
||||
* product + country + zone(if any) + weight band (insert when missing unless updateOnly).
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bms.price.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomerOption;
|
||||
// 提成功能暂停使用:import com.bms.price.domain.PrcProfitCommissionRate;
|
||||
import com.bms.price.domain.PrcProfitReportExport;
|
||||
@@ -14,6 +15,8 @@ public interface IPrcProfitReportService
|
||||
{
|
||||
List<BmsCustomerOption> selectCustomerOptions(String keywords);
|
||||
|
||||
Map<String, String> selectAreaRules();
|
||||
|
||||
List<PrcProfitReportOrderRow> selectOrderRows(PrcProfitReportQuery query);
|
||||
|
||||
List<PrcProfitReportProductRow> selectProductRows(PrcProfitReportQuery query);
|
||||
|
||||
+11
-9
@@ -2574,7 +2574,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
|
||||
Date sheetEffective = parseSheetEffectiveDate(sheet);
|
||||
List<BmsProductCountry> list = new ArrayList<>();
|
||||
int[] countryHeader = findHeaderCell(sheet, "国家");
|
||||
int[] countryHeader = findHeaderCell(sheet, "\u56fd\u5bb6");
|
||||
if (countryHeader == null)
|
||||
{
|
||||
return list;
|
||||
@@ -2582,12 +2582,12 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
int headerRowIdx = countryHeader[0];
|
||||
Row headerRow = sheet.getRow(headerRowIdx);
|
||||
int colCountry = countryHeader[1];
|
||||
int colWeight = findColByHeader(headerRow, "重量", colCountry + 1);
|
||||
int colFreight = findColByHeader(headerRow, "运费", colCountry + 1);
|
||||
int colRegister = findColByHeader(headerRow, "挂号", colCountry + 1);
|
||||
int colSize = findColByHeader(headerRow, "尺寸", colCountry + 1);
|
||||
int colLastMile = findColByHeader(headerRow, "尾程", colCountry + 1);
|
||||
int colLeadTime = findColByHeader(headerRow, "时效", colCountry + 1);
|
||||
int colWeight = findColByHeader(headerRow, "\u91cd\u91cf", colCountry + 1);
|
||||
int colFreight = findColByHeader(headerRow, "\u8fd0\u8d39", colCountry + 1);
|
||||
int colRegister = findColByHeader(headerRow, "\u6302\u53f7", colCountry + 1);
|
||||
int colSize = findColByHeader(headerRow, "\u5c3a\u5bf8", colCountry + 1);
|
||||
int colLastMile = findColByHeader(headerRow, "\u5c3e\u7a0b", colCountry + 1);
|
||||
int colLeadTime = findColByHeader(headerRow, "\u65f6\u6548", colCountry + 1);
|
||||
if (colWeight < 0)
|
||||
{
|
||||
colWeight = colCountry + 1;
|
||||
@@ -2627,7 +2627,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
String countryCell = cellString(row.getCell(colCountry));
|
||||
String weightLabel = cellString(row.getCell(colWeight));
|
||||
if ("国家".equals(countryCell) || weightLabel.contains("重量段"))
|
||||
if ("\u56fd\u5bb6".equals(countryCell)
|
||||
|| (weightLabel != null && weightLabel.contains("\u91cd\u91cf\u6bb5")))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -2847,7 +2848,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Matcher m = WEIGHT_RANGE_PATTERN.matcher(label.replace(" ", ""));
|
||||
String compact = label.replace(" ", "").replace("\u3000", "");
|
||||
Matcher m = WEIGHT_RANGE_PATTERN.matcher(compact);
|
||||
if (!m.find())
|
||||
{
|
||||
return null;
|
||||
|
||||
+19
@@ -972,6 +972,25 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
return localRebillAsyncRunner.get(taskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BatchAddIncomeBillFeeTask cancelLocalRebill(String taskId)
|
||||
{
|
||||
if (StringUtils.isEmpty(taskId))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.taskIdRequired"));
|
||||
}
|
||||
if (localRebillAsyncRunner == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.taskNotExists"));
|
||||
}
|
||||
BatchAddIncomeBillFeeTask task = localRebillAsyncRunner.requestCancel(taskId.trim());
|
||||
if (task == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.taskNotExists"));
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String submitOrderBillExport(String exportType, PrcOrderBill query)
|
||||
{
|
||||
|
||||
+91
-4
@@ -47,6 +47,7 @@ import com.bms.price.mapper.PrcPriceSetVersionMapper;
|
||||
import com.bms.price.mapper.PrcSurchargeMapper;
|
||||
import com.bms.price.mapper.PrcSurchargeRelMapper;
|
||||
import com.bms.price.service.IBmsProductService;
|
||||
import com.bms.price.service.support.ExclusiveHalaraQuoteExcelParser;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.search.PriceChangeEsLog;
|
||||
import com.bms.price.service.IPrcPriceExcelFileService;
|
||||
@@ -68,6 +69,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
private static final String STATUS_REJECT = "R";
|
||||
private static final String STATUS_STOP = "S";
|
||||
private static final String TYPE_FIXED = "FIXED";
|
||||
private static final String CATEGORY_PACKAGE = PrcPriceSet.CATEGORY_PACKAGE;
|
||||
private static final String CATEGORY_EXCLUSIVE = PrcPriceSet.CATEGORY_EXCLUSIVE;
|
||||
private static final String TYPE_DISCOUNT = "DISCOUNT";
|
||||
private static final String VER_DRAFT = "0";
|
||||
private static final String VER_EFFECT = "1";
|
||||
@@ -114,6 +117,9 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
@Autowired
|
||||
private IBmsProductService productService;
|
||||
|
||||
@Autowired
|
||||
private ExclusiveHalaraQuoteExcelParser exclusiveHalaraQuoteExcelParser;
|
||||
|
||||
@Lazy
|
||||
@Autowired
|
||||
private PrcPriceSetSaveAsyncRunner saveAsyncRunner;
|
||||
@@ -228,10 +234,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
set.setSetType(TYPE_FIXED);
|
||||
}
|
||||
set.setSetCategory(normalizeSetCategory(set.getSetCategory()));
|
||||
setMapper.insertPrcPriceSet(set);
|
||||
if (StringUtils.isEmpty(set.getPriceSetCode()))
|
||||
{
|
||||
set.setPriceSetCode(String.format("SET%04d", set.getPriceSetId()));
|
||||
set.setPriceSetCode(buildPriceSetCode(set.getSetCategory(), set.getPriceSetId()));
|
||||
setMapper.updatePrcPriceSet(set);
|
||||
}
|
||||
}
|
||||
@@ -240,6 +247,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
PrcPriceSet db = requireSet(set.getPriceSetId());
|
||||
assertBasicEditable(db);
|
||||
set.setStatus(db.getStatus());
|
||||
// Category is immutable after create; keep DB value for name uniqueness scope.
|
||||
set.setSetCategory(normalizeSetCategory(db.getSetCategory()));
|
||||
setMapper.updatePrcPriceSet(set);
|
||||
}
|
||||
reportSaveProgress(set, 20, "saving version");
|
||||
@@ -275,6 +284,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
throw new ServiceException(MessageUtils.message("price.set.name.empty"));
|
||||
}
|
||||
set.setPriceSetName(set.getPriceSetName().trim());
|
||||
set.setSetCategory(normalizeSetCategory(db.getSetCategory()));
|
||||
if (setMapper.countByName(set) > 0)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.name.exists"));
|
||||
@@ -641,6 +651,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
copy.setPriceSetName(src.getPriceSetName() + "-副本");
|
||||
copy.setPriceSetNote(src.getPriceSetNote());
|
||||
copy.setSetType(src.getSetType());
|
||||
copy.setSetCategory(normalizeSetCategory(src.getSetCategory()));
|
||||
copy.setDefaultDiscount(src.getDefaultDiscount());
|
||||
copy.setDefaultType(src.getDefaultType());
|
||||
copy.setCalculateSign(src.getCalculateSign());
|
||||
@@ -731,8 +742,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
set.setCustomers(db.getCustomers());
|
||||
}
|
||||
if (StringUtils.isEmpty(set.getSetCategory()))
|
||||
{
|
||||
set.setSetCategory(db.getSetCategory());
|
||||
}
|
||||
}
|
||||
}
|
||||
set.setSetCategory(normalizeSetCategory(set.getSetCategory()));
|
||||
|
||||
int nameCount = setMapper.countByName(set);
|
||||
if (StringUtils.isEmpty(set.getPriceSetName()))
|
||||
@@ -1201,9 +1217,18 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
@Override
|
||||
public Map<String, Object> previewImportDetailPrices(byte[] bytes, String filename, boolean updateOnly,
|
||||
List<PrcPriceSetDetail> existingDetails, boolean replaceByProductCountry, String countryCode) throws Exception
|
||||
{
|
||||
return previewImportDetailPrices(bytes, filename, updateOnly, existingDetails, replaceByProductCountry,
|
||||
countryCode, false);
|
||||
}
|
||||
|
||||
public Map<String, Object> previewImportDetailPrices(byte[] bytes, String filename, boolean updateOnly,
|
||||
List<PrcPriceSetDetail> existingDetails, boolean replaceByProductCountry, String countryCode,
|
||||
boolean exclusiveTemplate) throws Exception
|
||||
{
|
||||
boolean replaceScope = replaceByProductCountry || StringUtils.isNotEmpty(countryCode);
|
||||
Map<String, List<BmsProductCountry>> byProduct = parseSetImportExcel(bytes, filename, countryCode);
|
||||
Map<String, List<BmsProductCountry>> byProduct = parseSetImportExcel(bytes, filename, countryCode,
|
||||
exclusiveTemplate);
|
||||
if (byProduct == null || byProduct.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
|
||||
@@ -1391,6 +1416,14 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
public Map<String, Object> previewImportDetailPricesMany(List<byte[]> bytesList, List<String> names,
|
||||
boolean updateOnly, List<PrcPriceSetDetail> existingDetails, boolean replaceByProductCountry, String countryCode)
|
||||
throws Exception
|
||||
{
|
||||
return previewImportDetailPricesMany(bytesList, names, updateOnly, existingDetails, replaceByProductCountry,
|
||||
countryCode, false);
|
||||
}
|
||||
|
||||
public Map<String, Object> previewImportDetailPricesMany(List<byte[]> bytesList, List<String> names,
|
||||
boolean updateOnly, List<PrcPriceSetDetail> existingDetails, boolean replaceByProductCountry, String countryCode,
|
||||
boolean exclusiveTemplate) throws Exception
|
||||
{
|
||||
if (bytesList == null || bytesList.isEmpty())
|
||||
{
|
||||
@@ -1407,7 +1440,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
String filename = names != null && i < names.size() ? names.get(i) : null;
|
||||
last = previewImportDetailPrices(bytesList.get(i), filename, updateOnly, existing, replaceByProductCountry,
|
||||
countryCode);
|
||||
countryCode, exclusiveTemplate);
|
||||
Object details = last == null ? null : last.get("details");
|
||||
if (details instanceof List)
|
||||
{
|
||||
@@ -1429,6 +1462,21 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
private Map<String, List<BmsProductCountry>> parseSetImportExcel(byte[] bytes, String filename, String countryCode)
|
||||
throws Exception
|
||||
{
|
||||
return parseSetImportExcel(bytes, filename, countryCode, false);
|
||||
}
|
||||
|
||||
private Map<String, List<BmsProductCountry>> parseSetImportExcel(byte[] bytes, String filename, String countryCode,
|
||||
boolean exclusiveTemplate) throws Exception
|
||||
{
|
||||
if (exclusiveTemplate)
|
||||
{
|
||||
if (StringUtils.isNotEmpty(countryCode))
|
||||
{
|
||||
throw new ServiceException(
|
||||
"\u4e13\u5c5e\u62a5\u4ef7\u4e0d\u652f\u6301\u5355\u56fd\u5bfc\u5165\uff0c\u8bf7\u4e0a\u4f20\u5b8c\u6574\u5927\u5ba2\u6298\u6263\u62a5\u4ef7\u8868");
|
||||
}
|
||||
return exclusiveHalaraQuoteExcelParser.parse(bytes, filename);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(countryCode))
|
||||
{
|
||||
return productService.parseCountryQuoteExcelByProduct(bytes, filename, countryCode);
|
||||
@@ -1551,7 +1599,9 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
reportImportProgress(taskId, priceSetId, 20, "parsing excel");
|
||||
boolean replaceScope = replaceByProductCountry || StringUtils.isNotEmpty(countryCode);
|
||||
Map<String, List<BmsProductCountry>> byProduct = parseSetImportExcel(bytes, filename, countryCode);
|
||||
boolean exclusiveTemplate = CATEGORY_EXCLUSIVE.equals(normalizeSetCategory(db.getSetCategory()));
|
||||
Map<String, List<BmsProductCountry>> byProduct = parseSetImportExcel(bytes, filename, countryCode,
|
||||
exclusiveTemplate);
|
||||
if (byProduct == null || byProduct.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
|
||||
@@ -2746,6 +2796,15 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
throw new ServiceException(MessageUtils.message("price.set.discount.range"));
|
||||
}
|
||||
}
|
||||
if (StringUtils.isEmpty(set.getSetCategory()) && set.getPriceSetId() != null)
|
||||
{
|
||||
PrcPriceSet db = setMapper.selectPrcPriceSetHeaderById(set.getPriceSetId());
|
||||
if (db != null)
|
||||
{
|
||||
set.setSetCategory(db.getSetCategory());
|
||||
}
|
||||
}
|
||||
set.setSetCategory(normalizeSetCategory(set.getSetCategory()));
|
||||
if (setMapper.countByName(set) > 0)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.name.exists"));
|
||||
@@ -2756,6 +2815,34 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
}
|
||||
|
||||
/** PACKAGE (default) or EXCLUSIVE. */
|
||||
public static String normalizeSetCategory(String setCategory)
|
||||
{
|
||||
if (CATEGORY_EXCLUSIVE.equalsIgnoreCase(StringUtils.trim(setCategory)))
|
||||
{
|
||||
return CATEGORY_EXCLUSIVE;
|
||||
}
|
||||
return CATEGORY_PACKAGE;
|
||||
}
|
||||
|
||||
private static String buildPriceSetCode(String setCategory, Long priceSetId)
|
||||
{
|
||||
String prefix = CATEGORY_EXCLUSIVE.equals(normalizeSetCategory(setCategory)) ? "EXC" : "SET";
|
||||
return String.format("%s%04d", prefix, priceSetId);
|
||||
}
|
||||
|
||||
/** Ensure the package belongs to the expected module category. */
|
||||
public void assertSetCategory(Long priceSetId, String expectedCategory)
|
||||
{
|
||||
PrcPriceSet db = requireSet(priceSetId);
|
||||
String actual = normalizeSetCategory(db.getSetCategory());
|
||||
String expect = normalizeSetCategory(expectedCategory);
|
||||
if (!expect.equals(actual))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.notExists"));
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDetail(PrcPriceSet set, PrcPriceSetDetail detail)
|
||||
{
|
||||
if (StringUtils.isEmpty(detail.getProductCode()))
|
||||
|
||||
+20
-1
@@ -3,6 +3,7 @@ package com.bms.price.service.impl;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
// 提成功能暂停使用:import java.math.BigDecimal;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -19,6 +20,7 @@ import com.bms.price.domain.vo.PrcProfitReportOrderRow;
|
||||
import com.bms.price.domain.vo.PrcProfitReportProductRow;
|
||||
import com.bms.price.domain.vo.PrcProfitReportQuery;
|
||||
import com.bms.price.domain.vo.PrcProfitReportSummary;
|
||||
import com.bms.price.enums.ProfitAreaRule;
|
||||
import com.bms.price.mapper.PrcProfitReportMapper;
|
||||
import com.bms.price.service.IPrcProfitReportService;
|
||||
|
||||
@@ -48,6 +50,12 @@ public class PrcProfitReportServiceImpl implements IPrcProfitReportService
|
||||
return customerMapper.selectCustomerOptions(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> selectAreaRules()
|
||||
{
|
||||
return ProfitAreaRule.areaRules();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PrcProfitReportOrderRow> selectOrderRows(PrcProfitReportQuery query)
|
||||
{
|
||||
@@ -105,12 +113,23 @@ public class PrcProfitReportServiceImpl implements IPrcProfitReportService
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
return;
|
||||
throw new ServiceException("请选择客户区域");
|
||||
}
|
||||
if ("COMMISSION_MISSING".equals(query.getDetailType()))
|
||||
{
|
||||
throw new ServiceException("提成功能已暂停使用");
|
||||
}
|
||||
if (query.getCustomerArea() == null || query.getCustomerArea().trim().isEmpty())
|
||||
{
|
||||
throw new ServiceException("请选择客户区域");
|
||||
}
|
||||
ProfitAreaRule rule = ProfitAreaRule.fromCustomerArea(query.getCustomerArea());
|
||||
if (rule == null)
|
||||
{
|
||||
throw new ServiceException("该客户区域未配置毛利规则");
|
||||
}
|
||||
query.setCustomerArea(query.getCustomerArea().trim());
|
||||
query.setProfitRule(rule.name());
|
||||
query.setOrderNos(splitFilterValues(query.getOrderNo(), "订单号"));
|
||||
query.setCustomerCodes(splitFilterValues(query.getCustomerCode(), "客户代码"));
|
||||
query.setCustomerNames(splitFilterValues(query.getCustomerName(), "客户名称"));
|
||||
|
||||
+57
-9
@@ -410,12 +410,16 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
if (binds == null || binds.isEmpty())
|
||||
{
|
||||
String msgKey = request instanceof TmsPriceBillRequest
|
||||
? "price.bill.noCustomerPackage"
|
||||
: "price.quote.noCustomerPackage";
|
||||
throw new ServiceException(MessageUtils.message(msgKey));
|
||||
if (request instanceof TmsPriceBillRequest)
|
||||
{
|
||||
// Soft-fail: persist billing_fail_status=4 on the order bill.
|
||||
return buildBillingMissResult(request, productCode, productName, countryCode, countryName,
|
||||
weight, bizDate, BillBillingFailStatus.NO_CUSTOMER_PACKAGE.getCode());
|
||||
}
|
||||
throw new ServiceException(MessageUtils.message("price.quote.noCustomerPackage"));
|
||||
}
|
||||
|
||||
boolean anyEnabledSet = false;
|
||||
boolean anyActiveVersion = false;
|
||||
boolean productExcludedBySetProducts = false;
|
||||
boolean attemptedPriceMatch = false;
|
||||
@@ -428,10 +432,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
for (PrcCustomerPriceSet bind : binds)
|
||||
{
|
||||
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(bind.getPriceSetId());
|
||||
// Only enabled (E) sets participate; draft/stop are skipped and recorded as fail below.
|
||||
if (set == null || !"E".equals(set.getStatus()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
anyEnabledSet = true;
|
||||
// Temporal gate: prc_price_set_version.effective_time / expire_time (not bind pr_begindate)
|
||||
PrcPriceSetVersion version = versionMapper.selectActiveByPriceSetAt(set.getPriceSetId(), asOf);
|
||||
if (version == null || version.getPsvId() == null)
|
||||
@@ -560,9 +566,22 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
|
||||
if (!anyActiveVersion)
|
||||
{
|
||||
String msgKey = request instanceof TmsPriceBillRequest
|
||||
? "price.bill.noActivePackageVersion"
|
||||
: "price.quote.noActivePackageVersion";
|
||||
if (request instanceof TmsPriceBillRequest)
|
||||
{
|
||||
// Soft-fail: package bound but not enabled, or enabled with no version at bizDate.
|
||||
// Persists billing_fail_status=5 so local rebill / MQ bill both record the miss.
|
||||
TmsPriceQuoteResult miss = buildBillingMissResult(request, productCode, productName,
|
||||
countryCode, countryName, weight, bizDate,
|
||||
BillBillingFailStatus.NO_ACTIVE_PACKAGE_VERSION.getCode());
|
||||
if (!anyEnabledSet)
|
||||
{
|
||||
miss.setMatchMessage(MessageUtils.message("price.bill.billingFail.packageNotEnabled"));
|
||||
}
|
||||
return miss;
|
||||
}
|
||||
String msgKey = anyEnabledSet
|
||||
? "price.quote.noActivePackageVersion"
|
||||
: "price.quote.packageNotEnabled";
|
||||
throw new ServiceException(MessageUtils.message(msgKey));
|
||||
}
|
||||
if (!attemptedPriceMatch && productExcludedBySetProducts)
|
||||
@@ -720,6 +739,14 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
return MessageUtils.message("price.bill.billingFail.catalogWeight");
|
||||
}
|
||||
if (missCode == BillBillingFailStatus.NO_CUSTOMER_PACKAGE.getCode())
|
||||
{
|
||||
return MessageUtils.message("price.bill.billingFail.noCustomerPackage");
|
||||
}
|
||||
if (missCode == BillBillingFailStatus.NO_ACTIVE_PACKAGE_VERSION.getCode())
|
||||
{
|
||||
return MessageUtils.message("price.bill.billingFail.noActivePackageVersion");
|
||||
}
|
||||
return MessageUtils.message("price.bill.packagePriceNotHit");
|
||||
}
|
||||
|
||||
@@ -994,7 +1021,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
countryCode, countryName, zoneCode, weight);
|
||||
BigDecimal billWeight = weight;
|
||||
boolean snappedToMinBand = false;
|
||||
// Miss: if charge weight is below the lowest band, bill at that band's weight_min.
|
||||
boolean snappedUpToNextBand = false;
|
||||
// Miss: below lowest band -> bill at that band's weight_min.
|
||||
if (detail == null)
|
||||
{
|
||||
PrcPriceSetDetail minBand = detailMapper.selectFixedMinBand(
|
||||
@@ -1007,6 +1035,19 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
snappedToMinBand = true;
|
||||
}
|
||||
}
|
||||
// Miss in a gap (e.g. 1.001 between 0-1 and 1.01-2): ceil to next band's weight_min.
|
||||
if (detail == null)
|
||||
{
|
||||
PrcPriceSetDetail nextBand = detailMapper.selectFixedNextBandUp(
|
||||
psvId, productCode, countryCode, countryName, zoneCode, weight);
|
||||
if (nextBand != null && nextBand.getWeightMin() != null)
|
||||
{
|
||||
detail = nextBand;
|
||||
billWeight = nextBand.getWeightMin().setScale(3, RoundingMode.HALF_UP);
|
||||
snappedUpToNextBand = true;
|
||||
}
|
||||
}
|
||||
// Still no band (above last max, or no details): caller records WEIGHT miss.
|
||||
if (detail == null)
|
||||
{
|
||||
return null;
|
||||
@@ -1016,7 +1057,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return null;
|
||||
}
|
||||
BmsProductCountry useCatalog = catalog;
|
||||
if (snappedToMinBand)
|
||||
if (snappedToMinBand || snappedUpToNextBand)
|
||||
{
|
||||
BmsProductCountry rematched = softMatchCatalog(
|
||||
productCode, countryCode, countryName, zoneCode, billWeight, bizDate, resolvedZone);
|
||||
@@ -1039,6 +1080,13 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
r.setChargeWeight(weight);
|
||||
msg = msg + "; billed at min band " + billWeight.toPlainString() + " (persist original weight)";
|
||||
}
|
||||
else if (snappedUpToNextBand)
|
||||
{
|
||||
// Gap ceil: bill at next band min (1.001 -> 1.01); persist snapped charge_weight.
|
||||
r.setWeight(weight);
|
||||
r.setChargeWeight(billWeight);
|
||||
msg = msg + "; snapped up " + weight.toPlainString() + " -> " + billWeight.toPlainString();
|
||||
}
|
||||
r.setMatchMessage(msg);
|
||||
r.setCustomerId(bind.getCustomerId());
|
||||
return r;
|
||||
|
||||
+913
@@ -0,0 +1,913 @@
|
||||
package com.bms.price.service.support;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
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.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.usermodel.DateUtil;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.price.domain.BmsProduct;
|
||||
import com.bms.price.domain.BmsProductCountry;
|
||||
import com.bms.price.domain.PrcZone;
|
||||
import com.bms.price.mapper.PrcZoneMapper;
|
||||
import com.bms.price.util.ZoneCodeAbbrevs;
|
||||
import com.bms.system.domain.BmsRegion;
|
||||
import com.bms.system.mapper.BmsRegionMapper;
|
||||
|
||||
/**
|
||||
* Exclusive / large-customer (HALARA) quote Excel parser.
|
||||
* Completely separate from ordinary package / product quote Excel parsing.
|
||||
*
|
||||
* Catalog columns: category / country / level / product name / price sheet / product code / feature / lead time.
|
||||
* Price sheets: destination / origin / weight / freight(CNY/KG) / handling(CNY/PCS).
|
||||
*/
|
||||
@Component
|
||||
public class ExclusiveHalaraQuoteExcelParser
|
||||
{
|
||||
private static final String CATALOG_SHEET = "\u4ea7\u54c1\u76ee\u5f55";
|
||||
|
||||
/** Parse 0.01<=W<=0.5 / 0.3<W<=1 */
|
||||
private static final Pattern WEIGHT_W_PATTERN = Pattern.compile(
|
||||
"(\\d+(?:\\.\\d+)?)\\s*[<\u2264]=?\\s*W\\s*[<\u2264]=?\\s*(\\d+(?:\\.\\d+)?)",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/** Parse HALARA bands: 0-0.3kg / 0kg-1kg / 1.01kg-2kg */
|
||||
private static final Pattern WEIGHT_DASH_PATTERN = Pattern.compile(
|
||||
"(?i)(\\d+(?:\\.\\d+)?)\\s*(?:kg)?\\s*[-~\uFF5E\u2014\u2013]\\s*(\\d+(?:\\.\\d+)?)\\s*(?:kg)?");
|
||||
|
||||
@Autowired
|
||||
private BmsRegionMapper regionMapper;
|
||||
|
||||
@Autowired
|
||||
private PrcZoneMapper zoneMapper;
|
||||
|
||||
public Map<String, List<BmsProductCountry>> parse(byte[] bytes, String filename) throws Exception
|
||||
{
|
||||
if (bytes == null || bytes.length == 0)
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u5bfc\u5165\u6587\u4ef6\u4e3a\u7a7a");
|
||||
}
|
||||
if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx")
|
||||
|| filename.toLowerCase(Locale.ROOT).endsWith(".xls")))
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u4ec5\u652f\u6301 .xls / .xlsx");
|
||||
}
|
||||
try (InputStream is = new ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
||||
{
|
||||
assertHalaraFormat(workbook);
|
||||
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
||||
if (catalog == null)
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u7f3a\u5c11\u300c\u4ea7\u54c1\u76ee\u5f55\u300d");
|
||||
}
|
||||
List<BmsProduct> products = dedupeByCode(parseCatalog(catalog));
|
||||
if (products.isEmpty())
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u4ea7\u54c1\u76ee\u5f55\u65e0\u6709\u6548\u4ea7\u54c1");
|
||||
}
|
||||
|
||||
Map<String, List<BmsProduct>> ownersBySheet = new LinkedHashMap<String, List<BmsProduct>>();
|
||||
for (BmsProduct product : products)
|
||||
{
|
||||
if (product == null || StringUtils.isEmpty(product.getProductCode()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Sheet productSheet = findSheetForProduct(workbook, product);
|
||||
if (productSheet == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<BmsProduct> owners = ownersBySheet.get(productSheet.getSheetName());
|
||||
if (owners == null)
|
||||
{
|
||||
owners = new ArrayList<BmsProduct>();
|
||||
ownersBySheet.put(productSheet.getSheetName(), owners);
|
||||
}
|
||||
owners.add(product);
|
||||
}
|
||||
|
||||
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
|
||||
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
|
||||
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
|
||||
Map<String, List<BmsProductCountry>> out = new LinkedHashMap<String, List<BmsProductCountry>>();
|
||||
List<String> dupTips = new ArrayList<String>();
|
||||
|
||||
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
|
||||
{
|
||||
String sheetName = workbook.getSheetName(i);
|
||||
if (shouldSkipSheet(sheetName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Sheet sheet = workbook.getSheetAt(i);
|
||||
if (sheet == null || findHeaderCell(sheet, "\u76ee\u7684\u5730") == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<BmsProduct> owners = ownersBySheet.get(sheetName);
|
||||
if (owners == null || owners.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (BmsProduct owner : owners)
|
||||
{
|
||||
List<BmsProductCountry> rows = parsePriceSheet(sheet, owner, regionByNameCn, euRegions,
|
||||
zoneByCountryAndCode);
|
||||
String tip = dupCountryWeightTip(owner, rows);
|
||||
if (StringUtils.isNotEmpty(tip))
|
||||
{
|
||||
dupTips.add(tip);
|
||||
}
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String key = owner.getProductCode().trim().toUpperCase(Locale.ROOT);
|
||||
List<BmsProductCountry> existing = out.get(key);
|
||||
if (existing == null)
|
||||
{
|
||||
out.put(key, new ArrayList<BmsProductCountry>(rows));
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.addAll(rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!dupTips.isEmpty())
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u5b58\u5728\u91cd\u590d\u56fd\u5bb6+\u91cd\u91cf\u6bb5\uff1a<br/>" + String.join("<br/>", dupTips));
|
||||
}
|
||||
if (out.isEmpty())
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u672a\u89e3\u6790\u5230\u6709\u6548\u4ef7\u683c\u884c");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
private void assertHalaraFormat(Workbook workbook)
|
||||
{
|
||||
boolean hasDest = false;
|
||||
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
|
||||
{
|
||||
String sheetName = workbook.getSheetName(i);
|
||||
if (shouldSkipSheet(sheetName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Sheet sheet = workbook.getSheetAt(i);
|
||||
if (sheet != null && findHeaderCell(sheet, "\u76ee\u7684\u5730") != null)
|
||||
{
|
||||
hasDest = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasDest)
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u4ec5\u652f\u6301\u5927\u5ba2\u6298\u6263\u62a5\u4ef7\u8868\uff08\u542b\u300c\u76ee\u7684\u5730\u300d\u300c\u8fd0\u8d39\u300d\u300c\u64cd\u4f5c\u8d39\u300d\u5217\uff09\uff0c\u8bf7\u52ff\u4f7f\u7528\u666e\u901a\u5957\u9910 Excel");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldSkipSheet(String name)
|
||||
{
|
||||
if (StringUtils.isEmpty(name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
String n = name.trim();
|
||||
if (CATALOG_SHEET.equals(n))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if ("Sheet1".equalsIgnoreCase(n) || "Sheet2".equalsIgnoreCase(n))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (n.contains("\u90ae\u7f16") || n.contains("\u8d54\u507f") || n.contains("\u9000\u4ef6")
|
||||
|| n.contains("\u7533\u62a5"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return n.contains("\u7a0e\u7387") || n.toUpperCase(Locale.ROOT).contains("VAT");
|
||||
}
|
||||
|
||||
private List<BmsProduct> parseCatalog(Sheet sheet)
|
||||
{
|
||||
List<BmsProduct> list = new ArrayList<BmsProduct>();
|
||||
int[] header = findHeaderCell(sheet, "\u4ea7\u54c1\u4ee3\u7801");
|
||||
if (header == null)
|
||||
{
|
||||
header = findHeaderCell(sheet, "\u4ea7\u54c1\u540d\u79f0");
|
||||
}
|
||||
if (header == null)
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u300c\u4ea7\u54c1\u76ee\u5f55\u300d\u672a\u627e\u5230\u8868\u5934\u300c\u4ea7\u54c1\u4ee3\u7801\u300d/\u300c\u4ea7\u54c1\u540d\u79f0\u300d");
|
||||
}
|
||||
int headerRowIdx = header[0];
|
||||
Row head = sheet.getRow(headerRowIdx);
|
||||
int colCode = findColByHeader(head, "\u4ea7\u54c1\u4ee3\u7801", 0);
|
||||
int colName = findColByHeader(head, "\u4ea7\u54c1\u540d\u79f0", 0);
|
||||
int colLevel = findColByHeader(head, "\u7b49\u7ea7", 0);
|
||||
if (colLevel < 0)
|
||||
{
|
||||
colLevel = findColByHeader(head, "\u65f6\u6548", 0);
|
||||
}
|
||||
int colCargo = findColByHeader(head, "\u4ea7\u54c1\u7279\u6027", 0);
|
||||
if (colCargo < 0)
|
||||
{
|
||||
colCargo = findColByHeader(head, "\u8d70\u8d27\u5c5e\u6027", 0);
|
||||
}
|
||||
int colDest = findColByHeader(head, "\u56fd\u5bb6", 0);
|
||||
if (colDest < 0)
|
||||
{
|
||||
colDest = findColByHeader(head, "\u76ee\u7684", 0);
|
||||
}
|
||||
int colLead = findColByHeader(head, "\u53c2\u8003\u65f6\u6548", 0);
|
||||
if (colCode < 0 && colName < 0)
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u300c\u4ea7\u54c1\u76ee\u5f55\u300d\u7f3a\u5c11\u300c\u4ea7\u54c1\u4ee3\u7801\u300d/\u300c\u4ea7\u54c1\u540d\u79f0\u300d\u5217");
|
||||
}
|
||||
int lastRow = sheet.getLastRowNum();
|
||||
for (int i = headerRowIdx + 1; i <= lastRow; i++)
|
||||
{
|
||||
Row row = sheet.getRow(i);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String rawCode = colCode >= 0 ? cellString(row.getCell(colCode)) : "";
|
||||
String name = colName >= 0 ? cellString(row.getCell(colName)) : "";
|
||||
if (StringUtils.isEmpty(rawCode) && StringUtils.isEmpty(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ("\u4ea7\u54c1\u4ee3\u7801".equals(rawCode) || "\u4ea7\u54c1\u540d\u79f0".equals(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String code = normalizeCode(rawCode);
|
||||
if (StringUtils.isEmpty(code))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BmsProduct p = new BmsProduct();
|
||||
p.setProductCode(code);
|
||||
p.setProductName(StringUtils.isNotEmpty(name) ? name : code);
|
||||
if (colLevel >= 0)
|
||||
{
|
||||
p.setServiceLevel(cellString(row.getCell(colLevel)));
|
||||
}
|
||||
if (colCargo >= 0)
|
||||
{
|
||||
p.setCargoAttr(cellString(row.getCell(colCargo)));
|
||||
}
|
||||
if (colDest >= 0)
|
||||
{
|
||||
p.setDestRegion(cellString(row.getCell(colDest)));
|
||||
}
|
||||
if (colLead >= 0)
|
||||
{
|
||||
p.setUpdateNote(cellString(row.getCell(colLead)));
|
||||
}
|
||||
list.add(p);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** Keep dots in codes such as GB.PEXP / DE.PEXP. */
|
||||
private static String normalizeCode(String raw)
|
||||
{
|
||||
if (StringUtils.isEmpty(raw))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String s = raw.trim().replaceAll("[\\(\\uFF08][^\\)\\uFF09]*[\\)\\uFF09]", "").trim();
|
||||
if (StringUtils.isEmpty(s))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Matcher m = Pattern.compile("(?i)^([A-Z0-9._-]+)").matcher(s);
|
||||
if (!m.find())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return m.group(1).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private List<BmsProduct> dedupeByCode(List<BmsProduct> products)
|
||||
{
|
||||
Map<String, BmsProduct> map = new LinkedHashMap<String, BmsProduct>();
|
||||
for (BmsProduct p : products)
|
||||
{
|
||||
if (p == null || StringUtils.isEmpty(p.getProductCode()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String key = p.getProductCode().trim().toUpperCase(Locale.ROOT);
|
||||
if (!map.containsKey(key))
|
||||
{
|
||||
map.put(key, p);
|
||||
}
|
||||
}
|
||||
return new ArrayList<BmsProduct>(map.values());
|
||||
}
|
||||
|
||||
private Sheet findSheetForProduct(Workbook workbook, BmsProduct product)
|
||||
{
|
||||
if (workbook == null || product == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String wantName = product.getProductName();
|
||||
String wantCode = product.getProductCode();
|
||||
if (StringUtils.isNotEmpty(wantName))
|
||||
{
|
||||
Sheet exact = workbook.getSheet(wantName.trim());
|
||||
if (exact != null && !shouldSkipSheet(exact.getSheetName()))
|
||||
{
|
||||
return exact;
|
||||
}
|
||||
}
|
||||
String nameKey = normalizeKey(wantName);
|
||||
String codeKey = normalizeKey(wantCode);
|
||||
String nameBase = sheetNameBase(nameKey);
|
||||
Sheet best = null;
|
||||
int bestScore = 0;
|
||||
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
|
||||
{
|
||||
String sheetName = workbook.getSheetName(i);
|
||||
if (shouldSkipSheet(sheetName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String key = normalizeKey(sheetName);
|
||||
if (StringUtils.isEmpty(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int score = 0;
|
||||
if (StringUtils.isNotEmpty(nameKey) && key.equals(nameKey))
|
||||
{
|
||||
return workbook.getSheetAt(i);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(nameKey) && key.contains(nameKey))
|
||||
{
|
||||
score = 100;
|
||||
}
|
||||
else if (StringUtils.isNotEmpty(nameKey) && nameKey.contains(key) && key.length() >= 4)
|
||||
{
|
||||
score = 80;
|
||||
}
|
||||
else if (StringUtils.isNotEmpty(nameBase) && sheetNameBase(key).equals(nameBase)
|
||||
&& nameBase.length() >= 2)
|
||||
{
|
||||
// e.g. catalog "DE express-general" -> sheet "DE express"
|
||||
score = 70;
|
||||
}
|
||||
else if (StringUtils.isNotEmpty(codeKey) && key.contains(codeKey))
|
||||
{
|
||||
score = 60;
|
||||
}
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
best = workbook.getSheetAt(i);
|
||||
}
|
||||
}
|
||||
return bestScore >= 60 ? best : null;
|
||||
}
|
||||
|
||||
private static String normalizeKey(String s)
|
||||
{
|
||||
if (s == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
String out = s.trim().toLowerCase(Locale.ROOT);
|
||||
out = out.replace('\u2013', '-').replace('\u2014', '-').replace('\uFF0D', '-');
|
||||
out = out.replace(" ", "").replace("\u3000", "");
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String sheetNameBase(String normalizedKey)
|
||||
{
|
||||
if (StringUtils.isEmpty(normalizedKey))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
int idx = normalizedKey.lastIndexOf('-');
|
||||
if (idx <= 0)
|
||||
{
|
||||
return normalizedKey;
|
||||
}
|
||||
return normalizedKey.substring(0, idx);
|
||||
}
|
||||
|
||||
private List<BmsProductCountry> parsePriceSheet(Sheet sheet, BmsProduct product,
|
||||
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
||||
Map<String, PrcZone> zoneByCountryAndCode)
|
||||
{
|
||||
List<BmsProductCountry> list = new ArrayList<BmsProductCountry>();
|
||||
if (sheet == null)
|
||||
{
|
||||
return list;
|
||||
}
|
||||
int[] destHeader = findHeaderCell(sheet, "\u76ee\u7684\u5730");
|
||||
if (destHeader == null)
|
||||
{
|
||||
return list;
|
||||
}
|
||||
int headerRowIdx = destHeader[0];
|
||||
Row headerRow = sheet.getRow(headerRowIdx);
|
||||
int colDest = destHeader[1];
|
||||
int colWeight = findColByHeader(headerRow, "\u91cd\u91cf", colDest + 1);
|
||||
int colFreight = findColByHeader(headerRow, "\u8fd0\u8d39", colDest + 1);
|
||||
int colHandle = findColByHeader(headerRow, "\u64cd\u4f5c\u8d39", colDest + 1);
|
||||
if (colHandle < 0)
|
||||
{
|
||||
colHandle = findColByHeader(headerRow, "\u6302\u53f7", colDest + 1);
|
||||
}
|
||||
int colLead = findColByHeader(headerRow, "\u65f6\u6548", colDest + 1);
|
||||
int colFeature = findColByHeader(headerRow, "\u4ea7\u54c1\u7279\u6027", colDest + 1);
|
||||
int colRemark = findColByHeader(headerRow, "\u5907\u6ce8", colDest + 1);
|
||||
if (colWeight < 0)
|
||||
{
|
||||
colWeight = colDest + 2;
|
||||
}
|
||||
if (colFreight < 0)
|
||||
{
|
||||
colFreight = colDest + 3;
|
||||
}
|
||||
if (colHandle < 0)
|
||||
{
|
||||
colHandle = colDest + 4;
|
||||
}
|
||||
|
||||
String currentDest = null;
|
||||
String currentLead = null;
|
||||
String currentRemark = null;
|
||||
int lastRow = sheet.getLastRowNum();
|
||||
for (int i = headerRowIdx + 1; i <= lastRow; i++)
|
||||
{
|
||||
Row row = sheet.getRow(i);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String destCell = cellString(row.getCell(colDest));
|
||||
String weightLabel = cellString(row.getCell(colWeight));
|
||||
if ("\u76ee\u7684\u5730".equals(destCell) || (weightLabel != null && weightLabel.contains("\u91cd\u91cf\u6bb5")))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (StringUtils.isNotEmpty(destCell))
|
||||
{
|
||||
currentDest = destCell;
|
||||
currentLead = colLead >= 0 ? emptyToNull(cellString(row.getCell(colLead))) : null;
|
||||
currentRemark = colRemark >= 0 ? emptyToNull(cellString(row.getCell(colRemark))) : null;
|
||||
}
|
||||
if (StringUtils.isEmpty(currentDest))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (currentDest.startsWith("\u4ea7\u54c1\u5b9a\u4e49") || currentDest.startsWith("\u4e00\u3001")
|
||||
|| currentDest.startsWith("\u4e8c\u3001") || currentDest.contains("\u670d\u52a1\u6761\u4ef6"))
|
||||
{
|
||||
break;
|
||||
}
|
||||
BigDecimal[] range = parseWeightRange(weightLabel);
|
||||
if (range == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BigDecimal freight = cellDecimal(row.getCell(colFreight));
|
||||
BigDecimal handling = cellDecimal(row.getCell(colHandle));
|
||||
ZoneCodeAbbrevs.CountryZoneParts parts = ZoneCodeAbbrevs.splitCountryZone(currentDest);
|
||||
if (parts == null || StringUtils.isEmpty(parts.countryName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String baseCountry = parts.countryName;
|
||||
String zoneLabel = parts.zoneLabel;
|
||||
String feature = colFeature >= 0 ? emptyToNull(cellString(row.getCell(colFeature))) : null;
|
||||
String sizeOrRemark = feature != null ? feature : currentRemark;
|
||||
|
||||
BmsRegion region = resolveRegion(baseCountry, regionByNameCn);
|
||||
if (region != null)
|
||||
{
|
||||
list.add(buildRow(product, region.getCountryNameCn(), region.getCountryCode2(), zoneLabel,
|
||||
zoneByCountryAndCode, range, freight, handling, sizeOrRemark, currentRemark, currentLead));
|
||||
continue;
|
||||
}
|
||||
if (isEuName(baseCountry) && StringUtils.isEmpty(zoneLabel))
|
||||
{
|
||||
if (euRegions == null || euRegions.isEmpty())
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u89e3\u6790\u5230\u300c\u6b27\u76df\u300d\u4f46\u533a\u57df\u5e93\u65e0\u6b27\u76df\u56fd\u5bb6");
|
||||
}
|
||||
for (BmsRegion eu : euRegions)
|
||||
{
|
||||
list.add(buildRow(product, eu.getCountryNameCn(), eu.getCountryCode2(), null,
|
||||
zoneByCountryAndCode, range, freight, handling, sizeOrRemark, currentRemark, currentLead));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
list.add(buildRow(product, baseCountry, null, zoneLabel, zoneByCountryAndCode, range, freight, handling,
|
||||
sizeOrRemark, currentRemark, currentLead));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private BmsProductCountry buildRow(BmsProduct product, String countryName, String countryCode, String zoneLabel,
|
||||
Map<String, PrcZone> zoneByCountryAndCode, BigDecimal[] range, BigDecimal freight, BigDecimal handling,
|
||||
String sizeLimit, String lastMile, String leadTime)
|
||||
{
|
||||
BmsProductCountry c = new BmsProductCountry();
|
||||
if (product != null)
|
||||
{
|
||||
c.setProductCode(product.getProductCode());
|
||||
c.setProductName(product.getProductName());
|
||||
}
|
||||
c.setCountryName(countryName);
|
||||
c.setCountryCode(countryCode);
|
||||
applyZone(c, countryCode, zoneLabel, zoneByCountryAndCode);
|
||||
c.setWeightMin(range[0]);
|
||||
c.setWeightMax(range[1]);
|
||||
c.setFreightPrice(freight);
|
||||
c.setRegistrationFee(handling);
|
||||
c.setSizeLimit(sizeLimit);
|
||||
c.setLastMile(lastMile);
|
||||
c.setLeadTime(leadTime);
|
||||
return c;
|
||||
}
|
||||
|
||||
private void applyZone(BmsProductCountry c, String countryCode, String zoneLabel,
|
||||
Map<String, PrcZone> zoneByCountryAndCode)
|
||||
{
|
||||
if (StringUtils.isEmpty(zoneLabel))
|
||||
{
|
||||
c.setZoneId(null);
|
||||
c.setZoneCode(null);
|
||||
c.setZoneName(null);
|
||||
return;
|
||||
}
|
||||
String zoneCode = ZoneCodeAbbrevs.toAbbrev(zoneLabel);
|
||||
c.setZoneCode(zoneCode);
|
||||
PrcZone zone = null;
|
||||
if (StringUtils.isNotEmpty(countryCode) && zoneByCountryAndCode != null)
|
||||
{
|
||||
zone = zoneByCountryAndCode.get(countryCode.trim().toUpperCase(Locale.ROOT) + "|"
|
||||
+ zoneCode.trim().toUpperCase(Locale.ROOT));
|
||||
}
|
||||
if (zone != null)
|
||||
{
|
||||
c.setZoneId(zone.getZoneId());
|
||||
c.setZoneCode(zone.getZoneCode());
|
||||
c.setZoneName(zone.getZoneName());
|
||||
}
|
||||
else
|
||||
{
|
||||
c.setZoneId(null);
|
||||
c.setZoneName(StringUtils.isNotEmpty(c.getCountryName())
|
||||
? c.getCountryName() + "(" + zoneCode + ")"
|
||||
: zoneCode);
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal[] parseWeightRange(String label)
|
||||
{
|
||||
if (StringUtils.isEmpty(label))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String compact = label.replace(" ", "").replace("\u3000", "");
|
||||
Matcher m = WEIGHT_W_PATTERN.matcher(compact);
|
||||
if (!m.find())
|
||||
{
|
||||
m = WEIGHT_DASH_PATTERN.matcher(compact);
|
||||
if (!m.find())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
BigDecimal min = new BigDecimal(m.group(1));
|
||||
BigDecimal max = new BigDecimal(m.group(2));
|
||||
if (min.compareTo(max) > 0)
|
||||
{
|
||||
BigDecimal tmp = min;
|
||||
min = max;
|
||||
max = tmp;
|
||||
}
|
||||
return new BigDecimal[] { min, max };
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String dupCountryWeightTip(BmsProduct product, List<BmsProductCountry> rows)
|
||||
{
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Set<String> seen = new HashSet<String>();
|
||||
for (BmsProductCountry r : rows)
|
||||
{
|
||||
if (r == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String key = String.valueOf(r.getCountryCode()) + "|" + String.valueOf(r.getCountryName())
|
||||
+ "|" + String.valueOf(r.getZoneCode()) + "|"
|
||||
+ (r.getWeightMin() == null ? "" : r.getWeightMin().toPlainString()) + "-"
|
||||
+ (r.getWeightMax() == null ? "" : r.getWeightMax().toPlainString());
|
||||
if (!seen.add(key))
|
||||
{
|
||||
String name = product == null ? "" : product.getProductName();
|
||||
String code = product == null ? "" : product.getProductCode();
|
||||
return (StringUtils.isNotEmpty(name) ? name : code) + " \u91cd\u590d\u56fd\u5bb6+\u91cd\u91cf\u6bb5";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, BmsRegion> loadRegionByNameCn()
|
||||
{
|
||||
BmsRegion query = new BmsRegion();
|
||||
query.setStatus("0");
|
||||
List<BmsRegion> regions = regionMapper.selectRegionList(query);
|
||||
Map<String, BmsRegion> map = new HashMap<String, BmsRegion>();
|
||||
if (regions == null)
|
||||
{
|
||||
return map;
|
||||
}
|
||||
for (BmsRegion region : regions)
|
||||
{
|
||||
if (region == null || StringUtils.isEmpty(region.getCountryNameCn()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
map.put(region.getCountryNameCn().trim(), region);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private List<BmsRegion> loadEuRegions(Map<String, BmsRegion> regionByNameCn)
|
||||
{
|
||||
List<BmsRegion> list = new ArrayList<BmsRegion>();
|
||||
if (regionByNameCn == null)
|
||||
{
|
||||
return list;
|
||||
}
|
||||
Set<String> seen = new HashSet<String>();
|
||||
for (BmsRegion region : regionByNameCn.values())
|
||||
{
|
||||
if (region == null || !"1".equals(region.getIsEu()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (StringUtils.isEmpty(region.getCountryCode2()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String code = region.getCountryCode2().toUpperCase(Locale.ROOT);
|
||||
if (seen.add(code))
|
||||
{
|
||||
list.add(region);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private Map<String, PrcZone> loadZoneByCountryAndCode()
|
||||
{
|
||||
Map<String, PrcZone> map = new HashMap<String, PrcZone>();
|
||||
List<PrcZone> list = zoneMapper.selectPrcZoneList(new PrcZone());
|
||||
if (list == null)
|
||||
{
|
||||
return map;
|
||||
}
|
||||
for (PrcZone zone : list)
|
||||
{
|
||||
if (zone == null || StringUtils.isEmpty(zone.getZoneCode()) || StringUtils.isEmpty(zone.getCountryCode()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
map.put(zone.getCountryCode().trim().toUpperCase(Locale.ROOT) + "|"
|
||||
+ zone.getZoneCode().trim().toUpperCase(Locale.ROOT), zone);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private BmsRegion resolveRegion(String excelName, Map<String, BmsRegion> regionByNameCn)
|
||||
{
|
||||
if (StringUtils.isEmpty(excelName) || regionByNameCn == null || regionByNameCn.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String name = excelName.trim();
|
||||
BmsRegion exact = regionByNameCn.get(name);
|
||||
if (exact != null)
|
||||
{
|
||||
return exact;
|
||||
}
|
||||
List<BmsRegion> candidates = new ArrayList<BmsRegion>();
|
||||
for (Map.Entry<String, BmsRegion> e : regionByNameCn.entrySet())
|
||||
{
|
||||
String cn = e.getKey();
|
||||
if (cn == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (cn.contains(name) || name.contains(cn))
|
||||
{
|
||||
candidates.add(e.getValue());
|
||||
}
|
||||
}
|
||||
if (candidates.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (candidates.size() == 1)
|
||||
{
|
||||
return candidates.get(0);
|
||||
}
|
||||
BmsRegion best = candidates.get(0);
|
||||
for (int i = 1; i < candidates.size(); i++)
|
||||
{
|
||||
String cn = candidates.get(i).getCountryNameCn();
|
||||
if (cn != null && cn.length() > best.getCountryNameCn().length())
|
||||
{
|
||||
best = candidates.get(i);
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private static boolean isEuName(String name)
|
||||
{
|
||||
if (StringUtils.isEmpty(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
String n = name.trim();
|
||||
return "\u6b27\u76df".equals(n) || "EU".equalsIgnoreCase(n) || "\u6b27\u6d32\u8054\u76df".equals(n)
|
||||
|| "\u6b27\u76df\u56fd\u5bb6".equals(n) || "\u4e1c\u6b27".equals(n);
|
||||
}
|
||||
|
||||
private static int[] findHeaderCell(Sheet sheet, String headerText)
|
||||
{
|
||||
if (sheet == null || StringUtils.isEmpty(headerText))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
int lastRow = Math.min(sheet.getLastRowNum(), 30);
|
||||
int[] containsHit = null;
|
||||
for (int r = 0; r <= lastRow; r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int maxCol = Math.min(Math.max(row.getLastCellNum(), 12), 20);
|
||||
for (int c = 0; c < maxCol; c++)
|
||||
{
|
||||
String v = cellString(row.getCell(c));
|
||||
if (StringUtils.isEmpty(v))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (headerText.equals(v))
|
||||
{
|
||||
return new int[] { r, c };
|
||||
}
|
||||
if (containsHit == null && v.contains(headerText))
|
||||
{
|
||||
containsHit = new int[] { r, c };
|
||||
}
|
||||
}
|
||||
}
|
||||
return containsHit;
|
||||
}
|
||||
|
||||
private static int findColByHeader(Row headerRow, String keyword, int fromCol)
|
||||
{
|
||||
if (headerRow == null || StringUtils.isEmpty(keyword))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
int maxCol = Math.min(Math.max(headerRow.getLastCellNum(), fromCol + 8), 20);
|
||||
for (int c = Math.max(fromCol, 0); c < maxCol; c++)
|
||||
{
|
||||
String v = cellString(headerRow.getCell(c));
|
||||
if (StringUtils.isNotEmpty(v) && v.contains(keyword))
|
||||
{
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String cellString(Cell cell)
|
||||
{
|
||||
if (cell == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
CellType type = cell.getCellType();
|
||||
if (type == CellType.FORMULA)
|
||||
{
|
||||
type = cell.getCachedFormulaResultType();
|
||||
}
|
||||
switch (type)
|
||||
{
|
||||
case STRING:
|
||||
return cell.getStringCellValue() == null ? "" : cell.getStringCellValue().trim();
|
||||
case NUMERIC:
|
||||
if (DateUtil.isCellDateFormatted(cell))
|
||||
{
|
||||
Date d = cell.getDateCellValue();
|
||||
return d == null ? "" : new SimpleDateFormat("yyyy-MM-dd").format(d);
|
||||
}
|
||||
double num = cell.getNumericCellValue();
|
||||
if (Math.floor(num) == num)
|
||||
{
|
||||
return String.valueOf((long) num);
|
||||
}
|
||||
return String.valueOf(num);
|
||||
case BOOLEAN:
|
||||
return String.valueOf(cell.getBooleanCellValue());
|
||||
case BLANK:
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static BigDecimal cellDecimal(Cell cell)
|
||||
{
|
||||
if (cell == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
CellType type = cell.getCellType();
|
||||
if (type == CellType.FORMULA)
|
||||
{
|
||||
type = cell.getCachedFormulaResultType();
|
||||
}
|
||||
try
|
||||
{
|
||||
if (type == CellType.NUMERIC)
|
||||
{
|
||||
return BigDecimal.valueOf(cell.getNumericCellValue());
|
||||
}
|
||||
String s = cellString(cell);
|
||||
if (StringUtils.isEmpty(s))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
s = s.replace(",", "").replace("\uFFE5", "").replace("CNY", "").trim();
|
||||
if (StringUtils.isEmpty(s))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new BigDecimal(s);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String emptyToNull(String s)
|
||||
{
|
||||
return StringUtils.isEmpty(s) ? null : s;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -247,7 +247,7 @@ public final class OrderBillExportSupport
|
||||
case 24: return "产品价未匹配国家";
|
||||
case 25: return "产品价未匹配重量段";
|
||||
case 4: return "未绑定套餐";
|
||||
case 5: return "无生效套餐版本";
|
||||
case 5: return "套餐未启用/无生效版本";
|
||||
case 6: return "已生成应收账单";
|
||||
case 7: return "产品档案缺失";
|
||||
case 8: return "未命中价格";
|
||||
|
||||
+160
-8
@@ -6,13 +6,16 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -32,8 +35,9 @@ import com.bms.price.search.TabOrderSyncEsLog;
|
||||
import com.bms.price.service.IPrcOrderBillService;
|
||||
|
||||
/**
|
||||
* 仅根据 prc_order_bill 异步本地重计费(不走 Oracle / 不走 MQ)。
|
||||
* 大批量按 {@link #PAGE_SIZE} 分页处理;每一页在短生命周期线程池中执行。
|
||||
* Async local rebill from prc_order_bill only (no Oracle / MQ).
|
||||
* Large jobs page by {@link #PAGE_SIZE}; each page runs in a short-lived pool.
|
||||
* Cancel is cooperative: current in-flight bills finish, remaining bills are skipped.
|
||||
*/
|
||||
@Component
|
||||
public class PrcOrderBillLocalRebillAsyncRunner
|
||||
@@ -47,6 +51,11 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
public static final int MIN_THREADS = 2;
|
||||
public static final int MAX_THREADS = 16;
|
||||
private static final int FAIL_DETAIL_MAX = 200;
|
||||
/** Publish Redis progress at most this often while a page is running. */
|
||||
private static final int PROGRESS_EVERY_N = 20;
|
||||
private static final long PROGRESS_MIN_INTERVAL_MS = 500L;
|
||||
|
||||
private final ConcurrentHashMap<String, AtomicBoolean> cancelFlags = new ConcurrentHashMap<String, AtomicBoolean>();
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
@@ -76,22 +85,36 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
int total = billIds == null ? 0 : billIds.size();
|
||||
AtomicInteger success = new AtomicInteger(0);
|
||||
AtomicInteger fail = new AtomicInteger(0);
|
||||
AtomicInteger done = new AtomicInteger(0);
|
||||
AtomicLong lastProgressAt = new AtomicLong(0L);
|
||||
StringBuilder failTips = new StringBuilder();
|
||||
List<Map<String, Object>> failDetails = Collections.synchronizedList(new ArrayList<Map<String, Object>>());
|
||||
int threads = clampThreads(billingProperties == null ? null : billingProperties.getOrderSourceSyncThreads());
|
||||
cancelFlags.computeIfAbsent(taskId, k -> new AtomicBoolean(false));
|
||||
ExecutorService pool = newWorkerPool(threads, "local-rebill");
|
||||
try
|
||||
{
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, 1, "running", null, total, 0, 0);
|
||||
lastProgressAt.set(System.currentTimeMillis());
|
||||
if (total == 0)
|
||||
{
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_FAIL, 100, "failed", "no bills", 0, 0, 0);
|
||||
saveDoneLog(taskId, request, total, 0, 0, threads, failDetails, "FAIL", "no bills");
|
||||
return;
|
||||
}
|
||||
int processed = 0;
|
||||
if (isCancelRequested(taskId))
|
||||
{
|
||||
finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails);
|
||||
return;
|
||||
}
|
||||
boolean cancelled = false;
|
||||
for (int from = 0; from < total; from += PAGE_SIZE)
|
||||
{
|
||||
if (isCancelRequested(taskId))
|
||||
{
|
||||
cancelled = true;
|
||||
break;
|
||||
}
|
||||
int to = Math.min(from + PAGE_SIZE, total);
|
||||
List<String> page = billIds.subList(from, to);
|
||||
List<Callable<Boolean>> jobs = new ArrayList<Callable<Boolean>>(page.size());
|
||||
@@ -99,6 +122,10 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
{
|
||||
final String one = billId;
|
||||
jobs.add(() -> {
|
||||
if (isCancelRequested(taskId))
|
||||
{
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
try
|
||||
{
|
||||
orderBillService.rebillLocalByBillId(one);
|
||||
@@ -114,14 +141,27 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
taskId, one, e.getMessage());
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
finally
|
||||
{
|
||||
int finished = done.incrementAndGet();
|
||||
maybePublishProgress(taskId, total, finished, success.get(), fail.get(), lastProgressAt);
|
||||
}
|
||||
});
|
||||
}
|
||||
invokeAllQuiet(pool, jobs);
|
||||
processed = to;
|
||||
int percent = Math.min(99, (int) Math.floor(100.0 * processed / total));
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, percent,
|
||||
"processing " + processed + "/" + total + " page=" + ((from / PAGE_SIZE) + 1),
|
||||
null, total, success.get(), fail.get());
|
||||
if (isCancelRequested(taskId))
|
||||
{
|
||||
cancelled = true;
|
||||
break;
|
||||
}
|
||||
// Page boundary: always flush latest counts (even if throttle skipped mid-page).
|
||||
publishProgress(taskId, total, done.get(), success.get(), fail.get());
|
||||
lastProgressAt.set(System.currentTimeMillis());
|
||||
}
|
||||
if (cancelled || isCancelRequested(taskId))
|
||||
{
|
||||
finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails);
|
||||
return;
|
||||
}
|
||||
String msg = "success=" + success.get() + ", fail=" + fail.get();
|
||||
String err = fail.get() > 0 && failTips.length() > 0 ? failTips.toString() : null;
|
||||
@@ -135,6 +175,11 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (isCancelRequested(taskId))
|
||||
{
|
||||
finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails);
|
||||
return;
|
||||
}
|
||||
log.error("local rebill aborted taskId={}", taskId, e);
|
||||
String msg = errorMessage(e);
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_FAIL, 100, "failed", msg,
|
||||
@@ -144,10 +189,62 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
}
|
||||
finally
|
||||
{
|
||||
cancelFlags.remove(taskId);
|
||||
shutdownPool(pool);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request cooperative cancel. Returns current task snapshot (may still be RUNNING until page ends).
|
||||
*/
|
||||
public BatchAddIncomeBillFeeTask requestCancel(String taskId)
|
||||
{
|
||||
if (StringUtils.isEmpty(taskId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
BatchAddIncomeBillFeeTask task = get(taskId);
|
||||
if (task == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String st = task.getStatus();
|
||||
if (BatchAddIncomeBillFeeTask.STATUS_SUCCESS.equals(st)
|
||||
|| BatchAddIncomeBillFeeTask.STATUS_FAIL.equals(st)
|
||||
|| BatchAddIncomeBillFeeTask.STATUS_CANCELLED.equals(st))
|
||||
{
|
||||
return task;
|
||||
}
|
||||
cancelFlags.computeIfAbsent(taskId, k -> new AtomicBoolean(false)).set(true);
|
||||
// Keep RUNNING so clients keep polling until finishCancelled writes CANCELLED.
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING,
|
||||
Math.max(task.getPercent(), 1),
|
||||
"cancelling",
|
||||
null,
|
||||
task.getTotal(), task.getSuccess(), task.getFail());
|
||||
log.info("local rebill cancel requested taskId={} success={} fail={} total={}",
|
||||
taskId, task.getSuccess(), task.getFail(), task.getTotal());
|
||||
return get(taskId);
|
||||
}
|
||||
|
||||
private void finishCancelled(String taskId, LocalRebillRequest request, int total, int ok, int fail,
|
||||
int threads, List<Map<String, Object>> failDetails)
|
||||
{
|
||||
int percent = total <= 0 ? 100 : Math.min(100, (int) Math.floor(100.0 * (ok + fail) / total));
|
||||
String msg = "cancelled success=" + ok + ", fail=" + fail;
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_CANCELLED, Math.max(percent, 1), msg,
|
||||
"cancelled by user", total, ok, fail);
|
||||
saveDoneLog(taskId, request, total, ok, fail, threads, failDetails,
|
||||
EsBizLogSupport.STATUS_FAIL, "cancelled by user");
|
||||
log.info("local rebill cancelled taskId={} total={} success={} fail={}", taskId, total, ok, fail);
|
||||
}
|
||||
|
||||
private boolean isCancelRequested(String taskId)
|
||||
{
|
||||
AtomicBoolean flag = cancelFlags.get(taskId);
|
||||
return flag != null && flag.get();
|
||||
}
|
||||
|
||||
private void saveDoneLog(String taskId, LocalRebillRequest request, int total, int ok, int fail,
|
||||
int threads, List<Map<String, Object>> failDetails, String status, String errorMessage)
|
||||
{
|
||||
@@ -209,6 +306,24 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
public void put(String taskId, String status, int percent, String message, String error,
|
||||
int total, int success, int fail)
|
||||
{
|
||||
BatchAddIncomeBillFeeTask existing = get(taskId);
|
||||
if (existing != null
|
||||
&& BatchAddIncomeBillFeeTask.STATUS_CANCELLED.equals(existing.getStatus())
|
||||
&& !BatchAddIncomeBillFeeTask.STATUS_CANCELLED.equals(status))
|
||||
{
|
||||
// Keep cancelled: do not let a late RUNNING overwrite after user cancel.
|
||||
BatchAddIncomeBillFeeTask keep = new BatchAddIncomeBillFeeTask();
|
||||
keep.setTaskId(taskId);
|
||||
keep.setStatus(BatchAddIncomeBillFeeTask.STATUS_CANCELLED);
|
||||
keep.setPercent(Math.max(existing.getPercent(), percent));
|
||||
keep.setMessage(existing.getMessage());
|
||||
keep.setError(existing.getError());
|
||||
keep.setTotal(total > 0 ? total : existing.getTotal());
|
||||
keep.setSuccess(Math.max(existing.getSuccess(), success));
|
||||
keep.setFail(Math.max(existing.getFail(), fail));
|
||||
redisCache.setCacheObject(CACHE_KEY_PREFIX + taskId, keep, TTL_MINUTES, TimeUnit.MINUTES);
|
||||
return;
|
||||
}
|
||||
BatchAddIncomeBillFeeTask task = new BatchAddIncomeBillFeeTask();
|
||||
task.setTaskId(taskId);
|
||||
task.setStatus(status);
|
||||
@@ -226,6 +341,43 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttled Redis progress so the UI moves during a large page (not only after 500).
|
||||
* Does not change worker throughput; only how often status is published.
|
||||
*/
|
||||
private void maybePublishProgress(String taskId, int total, int finished, int ok, int fail,
|
||||
AtomicLong lastProgressAt)
|
||||
{
|
||||
if (total <= 0 || finished <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
boolean byCount = finished % PROGRESS_EVERY_N == 0 || finished >= total;
|
||||
long now = System.currentTimeMillis();
|
||||
long prev = lastProgressAt.get();
|
||||
boolean byTime = now - prev >= PROGRESS_MIN_INTERVAL_MS;
|
||||
if (!byCount && !byTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!lastProgressAt.compareAndSet(prev, now) && !byCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
publishProgress(taskId, total, finished, ok, fail);
|
||||
}
|
||||
|
||||
private void publishProgress(String taskId, int total, int finished, int ok, int fail)
|
||||
{
|
||||
int percent = total <= 0 ? 0 : Math.min(99, (int) Math.floor(100.0 * finished / total));
|
||||
if (finished >= total)
|
||||
{
|
||||
percent = Math.min(99, percent);
|
||||
}
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, percent,
|
||||
"processing " + finished + "/" + total, null, total, ok, fail);
|
||||
}
|
||||
|
||||
private static void invokeAllQuiet(ExecutorService pool, List<Callable<Boolean>> jobs) throws InterruptedException
|
||||
{
|
||||
List<Future<Boolean>> futures = pool.invokeAll(jobs);
|
||||
|
||||
@@ -11,7 +11,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="customerCodeDw" column="customer_code_dw" />
|
||||
<result property="creditCode" column="credit_code" />
|
||||
<result property="customerType" column="customer_type" />
|
||||
<result property="areaType" column="area_type" />
|
||||
<result property="customerArea" column="customer_area" />
|
||||
<result property="isEnable" column="is_enable" />
|
||||
<result property="salesUserId" column="sales_user_id" />
|
||||
<result property="salesUser" column="sales_user" />
|
||||
@@ -45,7 +45,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<!-- 账户余额/外币余额统一取 ctr_customer_account 余额表(CNY 为本币,其余为外币),不取 ctr_customer 冗余列 -->
|
||||
<!-- 信用额度=总额度+额外额度-已用额度-->
|
||||
<sql id="selectBmsCustomerVo">
|
||||
select c.customer_id, c.customer_code, c.customer_name, c.customer_code_dw, c.credit_code, c.customer_type, c.area_type,
|
||||
select c.customer_id, c.customer_code, c.customer_name, c.customer_code_dw, c.credit_code, c.customer_type, c.customer_area,
|
||||
c.is_enable,
|
||||
case when su.user_id is not null then cast(su.user_id as char) else null end as sales_user_id,
|
||||
su.nick_name as sales_user, c.ed, c.zq, c.settlement_user_id, c.settlement_user,
|
||||
@@ -92,7 +92,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</if>
|
||||
<if test="creditCode != null and creditCode != ''"> and c.credit_code = #{creditCode}</if>
|
||||
<if test="customerType != null and customerType != ''"> and c.customer_type = #{customerType}</if>
|
||||
<if test="areaType != null and areaType != ''"> and c.area_type = #{areaType}</if>
|
||||
<if test="customerArea != null and customerArea != ''"> and c.customer_area = #{customerArea}</if>
|
||||
<if test="isEnable != null and isEnable != ''"> and c.is_enable = #{isEnable}</if>
|
||||
<if test="salesUserId != null and salesUserId != ''"> and c.sales_user_id = #{salesUserId}</if>
|
||||
<if test="salesUser != null and salesUser != ''"> and su.nick_name = #{salesUser}</if>
|
||||
@@ -317,7 +317,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="customerCodeDw != null">customer_code_dw,</if>
|
||||
<if test="creditCode != null">credit_code,</if>
|
||||
<if test="customerType != null">customer_type,</if>
|
||||
<if test="areaType != null">area_type,</if>
|
||||
<if test="customerArea != null">customer_area,</if>
|
||||
<if test="isEnable != null">is_enable,</if>
|
||||
<if test="salesUserId != null">sales_user_id,</if>
|
||||
<if test="salesUser != null">sales_user,</if>
|
||||
@@ -350,7 +350,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="customerCodeDw != null">#{customerCodeDw},</if>
|
||||
<if test="creditCode != null">#{creditCode},</if>
|
||||
<if test="customerType != null">#{customerType},</if>
|
||||
<if test="areaType != null">#{areaType},</if>
|
||||
<if test="customerArea != null">#{customerArea},</if>
|
||||
<if test="isEnable != null">#{isEnable},</if>
|
||||
<if test="salesUserId != null">#{salesUserId},</if>
|
||||
<if test="salesUser != null">#{salesUser},</if>
|
||||
@@ -387,7 +387,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="customerCodeDw != null">customer_code_dw = #{customerCodeDw},</if>
|
||||
<if test="creditCode != null">credit_code = #{creditCode},</if>
|
||||
<if test="customerType != null">customer_type = #{customerType},</if>
|
||||
area_type = #{areaType},
|
||||
customer_area = #{customerArea},
|
||||
<if test="isEnable != null">is_enable = #{isEnable},</if>
|
||||
sales_user_id = #{salesUserId},
|
||||
sales_user = #{salesUser},
|
||||
|
||||
@@ -30,6 +30,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
del_flag, create_by, sales_user_id, source_sales_code
|
||||
<if test="customerCodeDw != null">, customer_code_dw</if>
|
||||
<if test="customerType != null">, customer_type</if>
|
||||
<if test="customerArea != null">, customer_area</if>
|
||||
<if test="salesUser != null">, sales_user</if>
|
||||
<if test="settlementUser != null">, settlement_user</if>
|
||||
<if test="serviceUser != null">, service_user</if>
|
||||
@@ -46,6 +47,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
'0', #{createBy}, #{salesUserId}, #{sourceSalesCode}
|
||||
<if test="customerCodeDw != null">, #{customerCodeDw}</if>
|
||||
<if test="customerType != null">, #{customerType}</if>
|
||||
<if test="customerArea != null">, #{customerArea}</if>
|
||||
<if test="salesUser != null">, #{salesUser}</if>
|
||||
<if test="settlementUser != null">, #{settlementUser}</if>
|
||||
<if test="serviceUser != null">, #{serviceUser}</if>
|
||||
@@ -66,6 +68,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="customerName != null">, customer_name = #{customerName}</if>
|
||||
<if test="customerCodeDw != null">, customer_code_dw = #{customerCodeDw}</if>
|
||||
<if test="customerType != null">, customer_type = #{customerType}</if>
|
||||
<if test="customerArea != null">, customer_area = #{customerArea}</if>
|
||||
, sales_user_id = #{salesUserId}
|
||||
, source_sales_code = #{sourceSalesCode}
|
||||
<if test="salesUser != null">, sales_user = #{salesUser}</if>
|
||||
|
||||
@@ -10,7 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
BELONG_SALER, BELONG_SALER_NAME, SETTLEMENT_MAN, SERVICE_MAN,
|
||||
REMARK, WEIGHT_LIMIT, CREDIT_LIMIT, TEMP_AMOUNT,
|
||||
ACCOUNT_AMOUNT, EUR_AMOUNT,
|
||||
CUSTOMER_TYPE, CUSTOMER_BALANCE_MODE, BL_OPEN,
|
||||
CUSTOMER_TYPE, CUSTOMER_AREA, CUSTOMER_BALANCE_MODE, BL_OPEN,
|
||||
CREATE_DATE, GMT_CREATE
|
||||
FROM WEISHI_USER.TAB_CUSTOMER
|
||||
</select>
|
||||
|
||||
@@ -66,7 +66,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="query.endTime != null and query.endTime != ''">
|
||||
AND o.PICKUP_DATE <= TO_DATE(#{query.endTime}, 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
AND o.BL_PICKUP = 1
|
||||
<choose>
|
||||
<when test="query.blPickup != null">
|
||||
AND o.BL_PICKUP = #{query.blPickup}
|
||||
</when>
|
||||
<!-- <otherwise>-->
|
||||
<!-- AND o.BL_PICKUP = 1-->
|
||||
<!-- </otherwise>-->
|
||||
</choose>
|
||||
<if test="query.orderStatuses != null and query.orderStatuses.size() > 0">
|
||||
AND o.ORDER_STATUS IN
|
||||
<foreach collection="query.orderStatuses" item="st" open="(" separator="," close=")">
|
||||
@@ -123,8 +130,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
o.TRACK_ID,
|
||||
o.CUSTOMER_CODE,
|
||||
o.CUSTOMER_NAME,
|
||||
o.PRODUCT_CODE,
|
||||
o.PRODUCT_NAME,
|
||||
<!-- List "产品" shows LINE_* (same as billing match), not PRODUCT_* floor name. -->
|
||||
o.LINE_CODE AS PRODUCT_CODE,
|
||||
o.LINE_NAME AS PRODUCT_NAME,
|
||||
o.PRODUCT_FLOOR_CODE,
|
||||
o.COUNTRY,
|
||||
o.COUNTRY_NAME,
|
||||
@@ -273,7 +281,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<foreach collection="batch" item="bid" open="(" separator="," close=")">
|
||||
#{bid}
|
||||
</foreach>
|
||||
AND o.BL_PICKUP = 1
|
||||
<choose>
|
||||
<when test="blPickup != null">
|
||||
AND o.BL_PICKUP = #{blPickup}
|
||||
</when>
|
||||
<!-- <otherwise>-->
|
||||
<!-- AND o.BL_PICKUP = 1-->
|
||||
<!-- </otherwise>-->
|
||||
</choose>
|
||||
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
|
||||
|
||||
@@ -519,6 +519,37 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!-- Gap between bands (e.g. 1.001 between 0-1 and 1.01-2): next higher weight_min. -->
|
||||
<select id="selectFixedNextBandUp" resultMap="PrcPriceSetDetailResult">
|
||||
<include refid="selectPrcPriceSetDetailVo"/>
|
||||
where psv_id = #{psvId}
|
||||
and ifnull(is_del, '0') = '0'
|
||||
and product_code = #{productCode}
|
||||
and weight_min is not null
|
||||
and weight_min > #{weight}
|
||||
<choose>
|
||||
<when test="countryCode != null and countryCode != ''">
|
||||
and upper(ifnull(country_code,'')) = upper(#{countryCode})
|
||||
</when>
|
||||
<when test="countryName != null and countryName != ''">
|
||||
and country_name = #{countryName}
|
||||
</when>
|
||||
<otherwise>
|
||||
and 1 = 0
|
||||
</otherwise>
|
||||
</choose>
|
||||
<choose>
|
||||
<when test="zoneCode != null and zoneCode != ''">
|
||||
and upper(ifnull(zone_code,'')) = upper(#{zoneCode})
|
||||
</when>
|
||||
<otherwise>
|
||||
and (zone_code is null or zone_code = '')
|
||||
</otherwise>
|
||||
</choose>
|
||||
order by weight_min asc, psd_id asc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="countZonedByPsvProductCountry" resultType="int">
|
||||
select count(1)
|
||||
from prc_price_set_detail
|
||||
|
||||
@@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="priceSetName" column="price_set_name" />
|
||||
<result property="priceSetNote" column="price_set_note" />
|
||||
<result property="setType" column="set_type" />
|
||||
<result property="setCategory" column="set_category" />
|
||||
<result property="defaultDiscount" column="default_discount" />
|
||||
<result property="defaultType" column="default_type" />
|
||||
<result property="calculateSign" column="calculate_sign" />
|
||||
@@ -26,7 +27,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<sql id="selectPrcPriceSetVo">
|
||||
select s.price_set_id, s.price_set_code, s.price_set_name, s.price_set_note,
|
||||
s.set_type, s.default_discount, s.default_type, s.calculate_sign,
|
||||
s.set_type, ifnull(s.set_category, 'PACKAGE') as set_category,
|
||||
s.default_discount, s.default_type, s.calculate_sign,
|
||||
s.status, s.tms_id,
|
||||
s.created_by, s.create_time, s.updated_by, s.update_time,
|
||||
(select count(distinct p.product_code)
|
||||
@@ -52,7 +54,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="selectPrcPriceSetHeaderById" parameterType="Long" resultMap="PrcPriceSetResult">
|
||||
select s.price_set_id, s.price_set_code, s.price_set_name, s.price_set_note,
|
||||
s.set_type, s.default_discount, s.default_type, s.calculate_sign,
|
||||
s.set_type, ifnull(s.set_category, 'PACKAGE') as set_category,
|
||||
s.default_discount, s.default_type, s.calculate_sign,
|
||||
s.status, s.tms_id,
|
||||
s.created_by, s.create_time, s.updated_by, s.update_time,
|
||||
null as product_count, null as customer_count, null as bound_customers
|
||||
@@ -66,6 +69,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="setType != null and setType != ''">
|
||||
AND s.set_type = #{setType}
|
||||
</if>
|
||||
<if test="setCategory != null and setCategory != ''">
|
||||
AND ifnull(s.set_category, 'PACKAGE') = #{setCategory}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
AND s.status = #{status}
|
||||
</if>
|
||||
@@ -126,6 +132,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="tmsId != null">
|
||||
AND tms_id = #{tmsId}
|
||||
</if>
|
||||
<if test="setCategory != null and setCategory != ''">
|
||||
AND ifnull(set_category, 'PACKAGE') = #{setCategory}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<insert id="insertPrcPriceSet" parameterType="PrcPriceSet" useGeneratedKeys="true" keyProperty="priceSetId">
|
||||
@@ -134,6 +143,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="priceSetName != null and priceSetName != ''">price_set_name,</if>
|
||||
<if test="priceSetNote != null">price_set_note,</if>
|
||||
<if test="setType != null and setType != ''">set_type,</if>
|
||||
<if test="setCategory != null and setCategory != ''">set_category,</if>
|
||||
<if test="defaultDiscount != null">default_discount,</if>
|
||||
<if test="defaultType != null">default_type,</if>
|
||||
<if test="calculateSign != null">calculate_sign,</if>
|
||||
@@ -146,6 +156,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="priceSetName != null and priceSetName != ''">#{priceSetName},</if>
|
||||
<if test="priceSetNote != null">#{priceSetNote},</if>
|
||||
<if test="setType != null and setType != ''">#{setType},</if>
|
||||
<if test="setCategory != null and setCategory != ''">#{setCategory},</if>
|
||||
<if test="defaultDiscount != null">#{defaultDiscount},</if>
|
||||
<if test="defaultType != null">#{defaultType},</if>
|
||||
<if test="calculateSign != null">#{calculateSign},</if>
|
||||
@@ -163,6 +174,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="priceSetName != null and priceSetName != ''">price_set_name = #{priceSetName},</if>
|
||||
<if test="priceSetNote != null">price_set_note = #{priceSetNote},</if>
|
||||
<if test="setType != null and setType != ''">set_type = #{setType},</if>
|
||||
<if test="setCategory != null and setCategory != ''">set_category = #{setCategory},</if>
|
||||
default_discount = #{defaultDiscount},
|
||||
<if test="defaultType != null">default_type = #{defaultType},</if>
|
||||
<if test="calculateSign != null">calculate_sign = #{calculateSign},</if>
|
||||
|
||||
@@ -119,6 +119,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<sql id="profitReportFromAndWhere">
|
||||
from prc_income_bill i
|
||||
inner join prc_order_bill p on p.bill_id = i.bill_id
|
||||
<if test="customerArea != null and customerArea != ''">
|
||||
left join ctr_customer c on c.customer_id = p.customer_id and c.del_flag = '0'
|
||||
</if>
|
||||
<!-- 提成表关联暂停使用,毛利报表不再依赖提成配置。
|
||||
left join prc_profit_commission_rate cr
|
||||
on cr.belong_saler_code = i.belong_saler_code and cr.status = '0'
|
||||
@@ -126,6 +129,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<where>
|
||||
<!-- 不将未匹配产品、国家或重量段的计费失败订单纳入毛利报表;保留历史空值(按成功处理)。 -->
|
||||
and (p.billing_fail_status is null or p.billing_fail_status not in (1, 2, 3))
|
||||
<if test="customerArea != null and customerArea != ''">
|
||||
and c.customer_area = #{customerArea}
|
||||
</if>
|
||||
<if test="beginBizDate != null">
|
||||
and p.biz_date >= #{beginBizDate}
|
||||
</if>
|
||||
@@ -194,8 +200,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</where>
|
||||
</sql>
|
||||
|
||||
<!-- 红线毛利 = 产品价 - 红线价;红线毛利率 = 红线毛利 / 红线价 * 100%。汇总先逐单计算再求和。 -->
|
||||
<!-- 广州缺少有效销售价或红线价时不计算毛利;计费失败留下的双零占位也不参与计算。 -->
|
||||
<sql id="guangzhouProfitAvailable">
|
||||
i.sales_amount is not null and i.redline_amount is not null
|
||||
and not (coalesce(p.billing_fail_status, 0) in (23, 24, 25) and i.sales_amount = 0 and i.redline_amount = 0)
|
||||
</sql>
|
||||
|
||||
<sql id="profitExpressions">
|
||||
<choose>
|
||||
<when test="profitRule == 'GUANGZHOU'">
|
||||
null as cost_amount, i.redline_amount,
|
||||
null as redline_gross_profit, null as redline_gross_margin_rate,
|
||||
i.sales_amount,
|
||||
case when <include refid="guangzhouProfitAvailable"/>
|
||||
then i.sales_amount - i.redline_amount else null end as sales_gross_profit,
|
||||
case when <include refid="guangzhouProfitAvailable"/> and i.sales_amount != 0
|
||||
then round((i.sales_amount - i.redline_amount) * 100 / i.sales_amount, 4)
|
||||
else null end as sales_gross_margin_rate
|
||||
</when>
|
||||
<otherwise>
|
||||
i.product_amount as cost_amount,
|
||||
i.redline_amount,
|
||||
case when i.product_amount is not null and i.redline_amount is not null
|
||||
@@ -207,11 +230,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
then i.sales_amount - i.product_amount else null end as sales_gross_profit,
|
||||
case when i.product_amount is not null and i.sales_amount is not null and i.sales_amount != 0
|
||||
then round((i.sales_amount - i.product_amount) * 100 / i.sales_amount, 4) else null end as sales_gross_margin_rate
|
||||
<!-- 提成比例与金额计算暂停使用;恢复时包含前置逗号。
|
||||
, cr.commission_rate,
|
||||
case when cr.commission_rate is not null and i.product_amount is not null and i.sales_amount is not null
|
||||
then round((i.sales_amount - i.product_amount) * cr.commission_rate / 100, 4) else null end as commission_amount
|
||||
-->
|
||||
</otherwise>
|
||||
</choose>
|
||||
</sql>
|
||||
|
||||
<select id="selectOrderRows" resultMap="PrcProfitReportOrderResult">
|
||||
@@ -228,9 +248,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
order by p.biz_date desc, p.bill_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectProductRows" resultMap="PrcProfitReportProductResult">
|
||||
select p.product_code, p.product_name, p.country_code, p.country_name,
|
||||
i.currency, count(*) as order_count,
|
||||
<!-- 汇总和产品维度使用同一套逐单有效数据计算,毛利率按有效销售额加权。 -->
|
||||
<sql id="profitAggregateExpressions">
|
||||
<choose>
|
||||
<when test="profitRule == 'GUANGZHOU'">
|
||||
null as cost_amount,
|
||||
sum(i.redline_amount) as redline_amount,
|
||||
null as redline_gross_profit, null as redline_gross_margin_rate,
|
||||
sum(i.sales_amount) as sales_amount,
|
||||
sum(case when <include refid="guangzhouProfitAvailable"/>
|
||||
then i.sales_amount - i.redline_amount else null end) as sales_gross_profit,
|
||||
case when sum(case when <include refid="guangzhouProfitAvailable"/>
|
||||
then i.sales_amount else null end) is null
|
||||
or sum(case when <include refid="guangzhouProfitAvailable"/>
|
||||
then i.sales_amount else null end) = 0
|
||||
then null
|
||||
else round(sum(case when <include refid="guangzhouProfitAvailable"/>
|
||||
then i.sales_amount - i.redline_amount else null end) * 100 /
|
||||
sum(case when <include refid="guangzhouProfitAvailable"/>
|
||||
then i.sales_amount else null end), 4)
|
||||
end as sales_gross_margin_rate
|
||||
</when>
|
||||
<otherwise>
|
||||
sum(i.product_amount) as cost_amount,
|
||||
sum(i.redline_amount) as redline_amount,
|
||||
sum(case when i.product_amount is not null and i.redline_amount is not null
|
||||
@@ -260,12 +299,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
sum(case when i.product_amount is not null and i.sales_amount is not null
|
||||
then i.sales_amount else null end), 4)
|
||||
end as sales_gross_margin_rate
|
||||
<!-- 提成金额与未计算订单数暂停使用。
|
||||
sum(case when cr.commission_rate is not null and i.product_amount is not null and i.sales_amount is not null
|
||||
then (i.sales_amount - i.product_amount) * cr.commission_rate / 100 else null end) as commission_amount,
|
||||
sum(case when cr.commission_rate is null or i.product_amount is null or i.sales_amount is null
|
||||
then 1 else 0 end) as commission_missing_order_count,
|
||||
-->
|
||||
</otherwise>
|
||||
</choose>
|
||||
</sql>
|
||||
|
||||
<select id="selectProductRows" resultMap="PrcProfitReportProductResult">
|
||||
select p.product_code, p.product_name, p.country_code, p.country_name,
|
||||
i.currency, count(*) as order_count,
|
||||
<include refid="profitAggregateExpressions"/>
|
||||
<include refid="profitReportFromAndWhere"/>
|
||||
group by p.product_code, p.product_name, p.country_code, p.country_name, i.currency
|
||||
order by p.product_code, p.country_code, i.currency
|
||||
@@ -273,41 +314,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="selectSummary" resultMap="PrcProfitReportSummaryResult">
|
||||
select i.currency, count(*) as order_count,
|
||||
sum(i.product_amount) as cost_amount,
|
||||
sum(i.redline_amount) as redline_amount,
|
||||
sum(case when i.product_amount is not null and i.redline_amount is not null
|
||||
then i.product_amount - i.redline_amount else null end) as redline_gross_profit,
|
||||
case when sum(case when i.product_amount is not null and i.redline_amount is not null
|
||||
then i.redline_amount else null end) is null
|
||||
or sum(case when i.product_amount is not null and i.redline_amount is not null
|
||||
then i.redline_amount else null end) = 0
|
||||
then null
|
||||
else round(
|
||||
sum(case when i.product_amount is not null and i.redline_amount is not null
|
||||
then i.product_amount - i.redline_amount else null end) * 100 /
|
||||
sum(case when i.product_amount is not null and i.redline_amount is not null
|
||||
then i.redline_amount else null end), 4)
|
||||
end as redline_gross_margin_rate,
|
||||
sum(i.sales_amount) as sales_amount,
|
||||
sum(case when i.product_amount is not null and i.sales_amount is not null
|
||||
then i.sales_amount - i.product_amount else null end) as sales_gross_profit,
|
||||
case when sum(case when i.product_amount is not null and i.sales_amount is not null
|
||||
then i.sales_amount else null end) is null
|
||||
or sum(case when i.product_amount is not null and i.sales_amount is not null
|
||||
then i.sales_amount else null end) = 0
|
||||
then null
|
||||
else round(
|
||||
sum(case when i.product_amount is not null and i.sales_amount is not null
|
||||
then i.sales_amount - i.product_amount else null end) * 100 /
|
||||
sum(case when i.product_amount is not null and i.sales_amount is not null
|
||||
then i.sales_amount else null end), 4)
|
||||
end as sales_gross_margin_rate
|
||||
<!-- 提成汇总计算暂停使用;恢复时包含前置逗号。
|
||||
, sum(case when cr.commission_rate is not null and i.product_amount is not null and i.sales_amount is not null
|
||||
then (i.sales_amount - i.product_amount) * cr.commission_rate / 100 else null end) as commission_amount,
|
||||
sum(case when cr.commission_rate is null or i.product_amount is null or i.sales_amount is null
|
||||
then 1 else 0 end) as commission_missing_order_count
|
||||
-->
|
||||
<include refid="profitAggregateExpressions"/>
|
||||
<include refid="profitReportFromAndWhere"/>
|
||||
<if test="arBillStatus != null and arBillStatus != ''">
|
||||
and p.ar_bill_status = #{arBillStatus}
|
||||
|
||||
Reference in New Issue
Block a user