Merge branch 'test' into feauture-20261104

# Conflicts:
#	bms-front/src/views/price/billing/components/BillDetail.vue
This commit is contained in:
liuminghui
2026-11-06 17:30:48 +08:00
52 changed files with 3529 additions and 456 deletions
@@ -450,6 +450,11 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
realChanges.add(c);
}
}
// Rebind/update with no field diff: skip empty UPDATE noise (full package save upsert)
if (existing != null && realChanges.isEmpty())
{
return;
}
saveLog(bind.getCustomerId(), customer == null ? null : customer.getCustomerCode(),
customer == null ? null : customer.getCustomerName(),
BmsCustomerLog.MODULE_PRICE_SET, newSet.getPriceSetName(),
@@ -0,0 +1,40 @@
package com.bms.price.airbill.domain;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 空运提单费用明细 srv_bill_air_fee。
*/
@Data
public class SrvBillAirFee
{
private Long id;
private String billCode;
/** 字典 bms_fee_type */
private String feeType;
private BigDecimal price;
/** 字典 bms_currency */
private String currency;
private String remark;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
private String updateBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
/** 0未删除 1已删除 */
private String delFlag;
}
@@ -0,0 +1,64 @@
package com.bms.price.airbill.domain;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 空运提单费用操作日志。
*/
@Data
public class SrvBillAirFeeLog
{
public static final String INSERT = "1";
public static final String UPDATE = "2";
public static final String DELETE = "3";
private Long logId;
private String billCode;
private Long feeId;
private String operationType;
private String summary;
private String changeContent;
private String createBy;
private String createByName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
private List<FieldChange> changes;
@Data
public static class FieldChange
{
private String field;
private String fieldLabel;
private String oldValue;
private String newValue;
public FieldChange()
{
}
public FieldChange(String field, String fieldLabel, String oldValue, String newValue)
{
this.field = field;
this.fieldLabel = fieldLabel;
this.oldValue = oldValue;
this.newValue = newValue;
}
}
}
@@ -0,0 +1,20 @@
package com.bms.price.airbill.domain;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* 保存提单费用时只带有变动的行。未出现的费用保持原样。
*/
@Data
public class SrvBillAirFeeSave
{
private String billCode;
private List<SrvBillAirFee> inserts = new ArrayList<>();
private List<SrvBillAirFee> updates = new ArrayList<>();
private List<Long> deletes = new ArrayList<>();
}
@@ -20,6 +20,11 @@ public class BillingProperties
private int consumerThreads = 8;
/** Concurrent threads for monitor order-source sync */
private int orderSourceSyncThreads = 8;
/**
* Concurrent threads for order-list local rebill (2~24).
* Keep below Druid maxActive with headroom for other traffic.
*/
private int localRebillThreads = 16;
/** Night window auto-retry of prc_billing_task (all statuses) */
private NightRetry nightRetry = new NightRetry();
private Oracle oracle = new Oracle();
@@ -84,6 +89,16 @@ public class BillingProperties
this.orderSourceSyncThreads = orderSourceSyncThreads;
}
public int getLocalRebillThreads()
{
return localRebillThreads;
}
public void setLocalRebillThreads(int localRebillThreads)
{
this.localRebillThreads = localRebillThreads;
}
public NightRetry getNightRetry()
{
return nightRetry;
@@ -40,6 +40,10 @@ public class BatchAddIncomeBillFeeRequest implements Serializable
private String beginBizDate;
private String endBizDate;
/** Receive/pickup time range (pickup_date), inclusive. */
private String beginPickupDate;
private String endPickupDate;
/** Surcharge config id (prc_surcharge.sc_id). */
@NotNull(message = "scId required")
private Long scId;
@@ -25,4 +25,7 @@ public class ChargeWeightTemplateRequest implements Serializable
private List<Integer> orderStatuses;
private String beginBizDate;
private String endBizDate;
/** Receive/pickup time range (pickup_date), inclusive. */
private String beginPickupDate;
private String endPickupDate;
}
@@ -56,7 +56,7 @@ public class PrcBillingBillListExportRow implements Serializable
@Excel(name = "计费时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date bizDate;
@Excel(name = "提货时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "收货时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date pickupDate;
@Excel(name = "销售员", width = 12)
@@ -31,4 +31,7 @@ public class RecalcBillPriceRequest implements Serializable
private List<Integer> billingFailStatuses;
private String beginBizDate;
private String endBizDate;
/** Receive/pickup time range (pickup_date), inclusive. */
private String beginPickupDate;
private String endPickupDate;
}
@@ -44,6 +44,9 @@ public interface PrcPriceSetDetailMapper
public int countByPsvId(Long psvId);
/** Count detail rows including soft-deleted (for deleted-version tab). */
public int countByPsvIdAll(Long psvId);
public int copyDetailsFromVersion(@Param("srcPsvId") Long srcPsvId, @Param("newPsvId") Long newPsvId);
/** Copy version effective time, expire time and status onto catalog detail rows. */
@@ -0,0 +1,28 @@
package com.bms.price.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.airbill.domain.SrvBillAirFee;
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
/**
* 空运提单费用及操作日志。
*/
public interface SrvBillAirFeeMapper
{
List<SrvBillAirFee> selectByBillCode(String billCode);
/** 只取这一票提单上仍有效的费用,供修改和删除前核对。 */
SrvBillAirFee selectLive(@Param("id") Long id, @Param("billCode") String billCode);
int insertFee(SrvBillAirFee fee);
int updateFee(SrvBillAirFee fee);
int deleteFee(@Param("id") Long id, @Param("billCode") String billCode, @Param("updateBy") String updateBy,
@Param("updateTime") java.util.Date updateTime);
int insertLog(SrvBillAirFeeLog log);
List<SrvBillAirFeeLog> selectLogs(String billCode);
}
@@ -1232,14 +1232,10 @@ 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(已收货). */
/** Always force BL_PICKUP=1; UI no longer exposes this filter. */
private static void normalizeBlPickup(TabOrderMonitorQuery query)
{
Integer blPickup = query.getBlPickup();
if (blPickup == null || (blPickup.intValue() != 0 && blPickup.intValue() != 1))
{
query.setBlPickup(Integer.valueOf(1));
}
query.setBlPickup(Integer.valueOf(1));
}
/**
@@ -3,7 +3,11 @@ package com.bms.price.search;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
@@ -148,6 +152,36 @@ public class EsBillSearchStore implements IBillSearchStore
@Override
public BillSearchPage search(String logType, String bizId, String status, int pageNum, int pageSize)
{
return search(logType, bizId, status, null, null, pageNum, pageSize);
}
@Override
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
int pageNum, int pageSize)
{
return search(logType, bizId, status, psvId, priceKind, null, null, pageNum, pageSize);
}
@Override
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, int pageNum, int pageSize)
{
return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, null, pageNum, pageSize);
}
@Override
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix, int pageNum, int pageSize)
{
return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, updateTimePrefix,
null, pageNum, pageSize);
}
@Override
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix, String countryKeyword,
int pageNum, int pageSize)
{
BillSearchPage page = new BillSearchPage();
if (pageNum < 1)
@@ -171,7 +205,8 @@ public class EsBillSearchStore implements IBillSearchStore
sortItem.put("updatedAt.keyword", sortOpt);
sort.add(sortItem);
body.put("sort", sort);
body.put("query", buildQuery(logType, bizId, status));
body.put("query", buildQuery(logType, bizId, status, psvId, priceKind, productCode, updateTime,
updateTimePrefix, countryKeyword));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<String>(body.toJSONString(), headers);
@@ -217,6 +252,108 @@ public class EsBillSearchStore implements IBillSearchStore
return page;
}
@Override
public PriceChangeGroupPage searchPriceChangeGroups(Long psvId, String priceKind, String status,
int pageNum, int pageSize)
{
PriceChangeGroupPage page = new PriceChangeGroupPage();
if (psvId == null)
{
return page;
}
if (pageNum < 1)
{
pageNum = 1;
}
if (pageSize < 1)
{
pageSize = 10;
}
List<BillSyncSnapshot> all = loadAllPriceChanges(psvId, priceKind, status, 10000);
Map<String, PriceChangeGroup> grouped = new LinkedHashMap<String, PriceChangeGroup>();
for (BillSyncSnapshot snap : all)
{
if (snap == null)
{
continue;
}
JSONObject req = parseRequestObject(snap.getRequestJson());
JSONObject keys = req == null ? null : req.getJSONObject("bizKeys");
String productCode = keys == null ? "" : StringUtils.nvl(keys.getString("productCode"), "");
String time = StringUtils.nvl(snap.getUpdateTime(), "");
String key = productCode + "|" + time;
PriceChangeGroup group = grouped.get(key);
if (group == null)
{
group = new PriceChangeGroup();
group.setProductCode(StringUtils.isEmpty(productCode) ? "-" : productCode);
group.setUpdateTime(time);
group.setAction(req == null ? null : req.getString("action"));
group.setOperator(req == null ? null : req.getString("operator"));
group.setStatus(snap.getStatus());
group.setChangeCount(0);
grouped.put(key, group);
}
group.setChangeCount(group.getChangeCount() + 1);
if (EsBizLogSupport.STATUS_FAIL.equals(snap.getStatus()))
{
group.setStatus(EsBizLogSupport.STATUS_FAIL);
}
}
List<PriceChangeGroup> list = new ArrayList<PriceChangeGroup>(grouped.values());
page.setTotal(list.size());
int from = (pageNum - 1) * pageSize;
if (from >= list.size())
{
return page;
}
int to = Math.min(from + pageSize, list.size());
page.setRows(new ArrayList<PriceChangeGroup>(list.subList(from, to)));
return page;
}
private List<BillSyncSnapshot> loadAllPriceChanges(Long psvId, String priceKind, String status, int max)
{
List<BillSyncSnapshot> all = new ArrayList<BillSyncSnapshot>();
int batch = 1000;
int pageNum = 1;
while (all.size() < max)
{
int size = Math.min(batch, max - all.size());
BillSearchPage chunk = search(EsLogType.PRICE_CHANGE, null, status, psvId, priceKind, null, null,
pageNum, size);
List<BillSyncSnapshot> rows = chunk.getRows();
if (rows == null || rows.isEmpty())
{
break;
}
all.addAll(rows);
if (rows.size() < size || all.size() >= chunk.getTotal())
{
break;
}
pageNum++;
}
return all;
}
private JSONObject parseRequestObject(String requestJson)
{
if (StringUtils.isEmpty(requestJson))
{
return null;
}
try
{
Object parsed = JSON.parse(requestJson);
return parsed instanceof JSONObject ? (JSONObject) parsed : null;
}
catch (Exception e)
{
return null;
}
}
private BillSyncSnapshot toSnapshot(JSONObject source, String id)
{
if (source == null)
@@ -249,12 +386,19 @@ public class EsBillSearchStore implements IBillSearchStore
return snap;
}
private JSONObject buildQuery(String logType, String bizId, String status)
private JSONObject buildQuery(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix, String countryKeyword)
{
JSONArray filters = new JSONArray();
addTerm(filters, "logType", logType);
addWildcard(filters, "bizId", bizId);
addTerm(filters, "status", status);
addPsvIdFilter(filters, psvId);
addTerm(filters, "request.priceKind", priceKind);
addProductCodeFilter(filters, productCode);
addCountryKeywordFilter(filters, countryKeyword);
addUpdateTimeFilter(filters, updateTime);
addUpdateTimePrefixFilter(filters, updateTimePrefix);
if (filters.isEmpty())
{
JSONObject matchAll = new JSONObject();
@@ -268,6 +412,195 @@ public class EsBillSearchStore implements IBillSearchStore
return query;
}
private void addProductCodeFilter(JSONArray filters, String productCode)
{
if (StringUtils.isEmpty(productCode))
{
return;
}
String code = productCode.trim();
JSONArray should = new JSONArray();
JSONObject termKw = new JSONObject();
termKw.put("request.bizKeys.productCode.keyword", code);
JSONObject wrapKw = new JSONObject();
wrapKw.put("term", termKw);
should.add(wrapKw);
JSONObject term = new JSONObject();
term.put("request.bizKeys.productCode", code);
JSONObject wrap = new JSONObject();
wrap.put("term", term);
should.add(wrap);
JSONObject inner = new JSONObject();
inner.put("should", should);
inner.put("minimum_should_match", Integer.valueOf(1));
JSONObject boolWrap = new JSONObject();
boolWrap.put("bool", inner);
filters.add(boolWrap);
}
/** Match country code / name (exact or contains). */
private void addCountryKeywordFilter(JSONArray filters, String countryKeyword)
{
if (StringUtils.isEmpty(countryKeyword))
{
return;
}
String kw = countryKeyword.trim();
String wild = "*" + escapeWildcard(kw) + "*";
JSONArray should = new JSONArray();
addShouldTerm(should, "request.bizKeys.countryCode.keyword", kw);
addShouldTerm(should, "request.bizKeys.countryCode", kw);
addShouldWildcard(should, "request.bizKeys.countryCode.keyword", wild);
addShouldWildcard(should, "request.bizKeys.countryCode", wild);
addShouldWildcard(should, "request.bizKeys.countryName.keyword", wild);
addShouldWildcard(should, "request.bizKeys.countryName", wild);
JSONObject matchName = new JSONObject();
JSONObject matchField = new JSONObject();
matchField.put("query", kw);
matchName.put("request.bizKeys.countryName", matchField);
JSONObject matchWrap = new JSONObject();
matchWrap.put("match", matchName);
should.add(matchWrap);
JSONObject inner = new JSONObject();
inner.put("should", should);
inner.put("minimum_should_match", Integer.valueOf(1));
JSONObject boolWrap = new JSONObject();
boolWrap.put("bool", inner);
filters.add(boolWrap);
}
private void addShouldTerm(JSONArray should, String field, String value)
{
JSONObject term = new JSONObject();
term.put(field, value);
JSONObject wrap = new JSONObject();
wrap.put("term", term);
should.add(wrap);
}
private void addShouldWildcard(JSONArray should, String field, String value)
{
JSONObject wild = new JSONObject();
wild.put(field, value);
JSONObject wrap = new JSONObject();
wrap.put("wildcard", wild);
should.add(wrap);
}
private static String escapeWildcard(String raw)
{
if (raw == null)
{
return "";
}
return raw.replace("\\", "\\\\").replace("*", "\\*").replace("?", "\\?");
}
private void addUpdateTimeFilter(JSONArray filters, String updateTime)
{
if (StringUtils.isEmpty(updateTime))
{
return;
}
String time = updateTime.trim();
JSONArray should = new JSONArray();
JSONObject t1 = new JSONObject();
t1.put("updateTime.keyword", time);
JSONObject w1 = new JSONObject();
w1.put("term", t1);
should.add(w1);
JSONObject t2 = new JSONObject();
t2.put("updatedAt.keyword", time);
JSONObject w2 = new JSONObject();
w2.put("term", t2);
should.add(w2);
JSONObject t3 = new JSONObject();
t3.put("updateTime", time);
JSONObject w3 = new JSONObject();
w3.put("term", t3);
should.add(w3);
JSONObject t4 = new JSONObject();
t4.put("updatedAt", time);
JSONObject w4 = new JSONObject();
w4.put("term", t4);
should.add(w4);
JSONObject inner = new JSONObject();
inner.put("should", should);
inner.put("minimum_should_match", Integer.valueOf(1));
JSONObject wrap = new JSONObject();
wrap.put("bool", inner);
filters.add(wrap);
}
/** Match write-time by minute prefix, e.g. 2026-11-05 13:27. */
private void addUpdateTimePrefixFilter(JSONArray filters, String updateTimePrefix)
{
if (StringUtils.isEmpty(updateTimePrefix))
{
return;
}
String prefix = escapeWildcard(updateTimePrefix.trim()) + "*";
JSONArray should = new JSONArray();
for (String field : new String[] { "updateTime.keyword", "updatedAt.keyword", "updateTime", "updatedAt" })
{
JSONObject opts = new JSONObject();
opts.put("value", prefix);
opts.put("case_insensitive", Boolean.TRUE);
JSONObject wildcard = new JSONObject();
wildcard.put(field, opts);
JSONObject wrap = new JSONObject();
wrap.put("wildcard", wildcard);
should.add(wrap);
}
JSONObject inner = new JSONObject();
inner.put("should", should);
inner.put("minimum_should_match", Integer.valueOf(1));
JSONObject wrap = new JSONObject();
wrap.put("bool", inner);
filters.add(wrap);
}
/** Prefer numeric term; also accept keyword/string mapping of bizKeys.psvId. */
private void addPsvIdFilter(JSONArray filters, Long psvId)
{
if (psvId == null)
{
return;
}
JSONArray should = new JSONArray();
JSONObject termNum = new JSONObject();
termNum.put("request.bizKeys.psvId", psvId);
JSONObject wrapNum = new JSONObject();
wrapNum.put("term", termNum);
should.add(wrapNum);
JSONObject termKw = new JSONObject();
termKw.put("request.bizKeys.psvId.keyword", String.valueOf(psvId));
JSONObject wrapKw = new JSONObject();
wrapKw.put("term", termKw);
should.add(wrapKw);
JSONObject termStr = new JSONObject();
termStr.put("request.bizKeys.psvId", String.valueOf(psvId));
JSONObject wrapStr = new JSONObject();
wrapStr.put("term", termStr);
should.add(wrapStr);
JSONObject inner = new JSONObject();
inner.put("should", should);
inner.put("minimum_should_match", Integer.valueOf(1));
JSONObject wrap = new JSONObject();
wrap.put("bool", inner);
filters.add(wrap);
}
private void addTerm(JSONArray filters, String field, String value)
{
if (StringUtils.isEmpty(value))
@@ -297,11 +630,6 @@ public class EsBillSearchStore implements IBillSearchStore
filters.add(wrap);
}
private String escapeWildcard(String value)
{
return value.replace("\\", "\\\\").replace("*", "\\*").replace("?", "\\?");
}
private long readTotal(Object total)
{
if (total instanceof JSONObject)
@@ -17,4 +17,48 @@ public interface IBillSearchStore
BillSyncSnapshot getByDocId(String docId);
BillSearchPage search(String logType, String bizId, String status, int pageNum, int pageSize);
/**
* Search with optional sales-version / priceKind filters (request.bizKeys.psvId, request.priceKind).
*/
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
int pageNum, int pageSize)
{
return search(logType, bizId, status, psvId, priceKind, null, null, pageNum, pageSize);
}
/**
* Search with optional productCode + updateTime (exact) / updateTimePrefix (e.g. yyyy-MM-dd HH:mm) filters.
*/
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, int pageNum, int pageSize)
{
return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, null, pageNum, pageSize);
}
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix, int pageNum, int pageSize)
{
return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, updateTimePrefix,
null, pageNum, pageSize);
}
/**
* Search with optional countryKeyword (matches bizKeys.countryCode / countryName).
*/
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix, String countryKeyword,
int pageNum, int pageSize)
{
return search(logType, bizId, status, pageNum, pageSize);
}
/**
* Group PRICE_CHANGE logs by productCode + updateTime for one package version.
*/
default PriceChangeGroupPage searchPriceChangeGroups(Long psvId, String priceKind, String status,
int pageNum, int pageSize)
{
return new PriceChangeGroupPage();
}
}
@@ -25,12 +25,33 @@ public final class PriceChangeEsLog
public static final String ACTION_DELETE = "DELETE";
public static final String ACTION_IMPORT = "IMPORT";
/** Async import workers have no SecurityContext; pass operName via this override. */
private static final ThreadLocal<String> OPERATOR_OVERRIDE = new ThreadLocal<String>();
private PriceChangeEsLog()
{
}
public static void beginOperator(String operName)
{
if (StringUtils.isNotEmpty(operName))
{
OPERATOR_OVERRIDE.set(operName.trim());
}
}
public static void endOperator()
{
OPERATOR_OVERRIDE.remove();
}
public static String operator()
{
String override = OPERATOR_OVERRIDE.get();
if (StringUtils.isNotEmpty(override))
{
return override;
}
try
{
String name = SecurityUtils.getUsername();
@@ -131,11 +152,31 @@ public final class PriceChangeEsLog
keys.put("countryCode", d.getCountryCode());
keys.put("countryName", d.getCountryName());
keys.put("zoneCode", d.getZoneCode());
keys.put("zoneName", d.getZoneName());
keys.put("weightMin", d.getWeightMin());
keys.put("weightMax", d.getWeightMax());
keys.put("weightLabel", weightBandOf(d));
return keys;
}
/** Display weight band for sales detail logs (e.g. 0&lt;W≤0.5). */
public static String weightBandOf(PrcPriceSetDetail d)
{
if (d == null)
{
return "";
}
if (StringUtils.isNotEmpty(d.getWeightLabel()))
{
return d.getWeightLabel();
}
if (d.getWeightMin() == null && d.getWeightMax() == null)
{
return "";
}
return stringify(d.getWeightMin()) + "<W\u2264" + stringify(d.getWeightMax());
}
public static void save(IBillSearchStore store, String bizId, String priceKind, String action,
Map<String, Object> bizKeys, List<Map<String, String>> changes)
{
@@ -0,0 +1,74 @@
package com.bms.price.search;
/**
* Price-change log group key: productCode + write time.
*/
public class PriceChangeGroup
{
private String productCode;
private String updateTime;
private String action;
private String operator;
private String status;
private int changeCount;
public String getProductCode()
{
return productCode;
}
public void setProductCode(String productCode)
{
this.productCode = productCode;
}
public String getUpdateTime()
{
return updateTime;
}
public void setUpdateTime(String updateTime)
{
this.updateTime = updateTime;
}
public String getAction()
{
return action;
}
public void setAction(String action)
{
this.action = action;
}
public String getOperator()
{
return operator;
}
public void setOperator(String operator)
{
this.operator = operator;
}
public String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
public int getChangeCount()
{
return changeCount;
}
public void setChangeCount(int changeCount)
{
this.changeCount = changeCount;
}
}
@@ -0,0 +1,31 @@
package com.bms.price.search;
import java.util.ArrayList;
import java.util.List;
/** Paginated product+time groups of price-change logs. */
public class PriceChangeGroupPage
{
private long total;
private List<PriceChangeGroup> rows = new ArrayList<PriceChangeGroup>();
public long getTotal()
{
return total;
}
public void setTotal(long total)
{
this.total = total;
}
public List<PriceChangeGroup> getRows()
{
return rows;
}
public void setRows(List<PriceChangeGroup> rows)
{
this.rows = rows == null ? new ArrayList<PriceChangeGroup>() : rows;
}
}
@@ -159,7 +159,7 @@ public interface IPrcOrderBillService
/**
* Async local rebill by selected billIds or list filters. Returns taskId for progress poll.
* Large sets: only bill_id loaded, then processed in pages of 500 with parallel workers.
* Large sets: only bill_id loaded, then processed by continuous parallel workers.
*/
String submitLocalRebill(LocalRebillRequest request);
@@ -0,0 +1,19 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.airbill.domain.SrvBillAirFee;
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
import com.bms.price.airbill.domain.SrvBillAirFeeSave;
/**
* 空运提单费用维护。
*/
public interface ISrvBillAirFeeService
{
List<SrvBillAirFee> listFees(String billCode);
List<SrvBillAirFeeLog> listLogs(String billCode);
/** 只写入有变动的行,并按行记录修改前后。返回给页面的提示。 */
String save(SrvBillAirFeeSave command);
}
@@ -183,8 +183,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
private static final int BATCH_ADD_FEE_PREVIEW_LIMIT = 20;
private static final int CHARGE_WEIGHT_IMPORT_MAX = 5000;
private static final int RECALC_PRICE_MAX = 5000;
/** Soft max for one local-rebill job (id-only load + paged parallel). */
private static final int LOCAL_REBILL_MAX = 100000;
/** Soft max for one local-rebill job (id-only load + continuous workers). */
private static final int LOCAL_REBILL_MAX = 200000;
/** Soft max for one order/billing list Excel export. */
private static final int ORDER_BILL_EXPORT_MAX = 50000;
@@ -681,6 +681,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
request.getCustomerCodes(), request.getProductCodes(), request.getCountryCode(),
request.getCreditArrearsStatus(), request.getOrderStatuses(), request.getBillingFailStatuses(),
request.getBeginBizDate(), request.getEndBizDate(),
request.getBeginPickupDate(), request.getEndPickupDate(),
"price.bill.recalc.filterRequired", "price.bill.recalc.scopeRequired",
"price.bill.recalc.selectedRequired");
if (bills.isEmpty())
@@ -1276,6 +1277,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
request.getCustomerCodes(), request.getProductCodes(), request.getCountryCode(),
request.getCreditArrearsStatus(), request.getOrderStatuses(), request.getBillingFailStatuses(),
request.getBeginBizDate(), request.getEndBizDate(),
request.getBeginPickupDate(), request.getEndPickupDate(),
"price.bill.fee.batch.filterRequired", "price.bill.fee.batch.scopeRequired",
"price.bill.fee.batch.selectedRequired");
}
@@ -1290,6 +1292,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
List<String> customerCodesRaw, List<String> productCodesRaw, String countryCodeRaw,
String creditArrearsStatusRaw, List<Integer> orderStatusesRaw, List<Integer> billingFailStatusesRaw,
String beginBizDateRaw, String endBizDateRaw,
String beginPickupDateRaw, String endPickupDateRaw,
String msgFilter, String msgScope, String msgSelected)
{
String scopeMode = StringUtils.nvl(scopeModeRaw, "").trim().toUpperCase();
@@ -1311,6 +1314,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
: countryCodeRaw.trim().toUpperCase();
String beginBizDate = StringUtils.isEmpty(beginBizDateRaw) ? null : beginBizDateRaw.trim();
String endBizDate = StringUtils.isEmpty(endBizDateRaw) ? null : endBizDateRaw.trim();
String beginPickupDate = StringUtils.isEmpty(beginPickupDateRaw) ? null : beginPickupDateRaw.trim();
String endPickupDate = StringUtils.isEmpty(endPickupDateRaw) ? null : endPickupDateRaw.trim();
boolean hasFilter = (orderNos != null && !orderNos.isEmpty())
|| (trackingNos != null && !trackingNos.isEmpty())
|| (transferNos != null && !transferNos.isEmpty())
@@ -1321,7 +1326,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|| StringUtils.isNotEmpty(creditArrearsStatus)
|| StringUtils.isNotEmpty(countryCode)
|| StringUtils.isNotEmpty(beginBizDate)
|| StringUtils.isNotEmpty(endBizDate);
|| StringUtils.isNotEmpty(endBizDate)
|| StringUtils.isNotEmpty(beginPickupDate)
|| StringUtils.isNotEmpty(endPickupDate);
if (!hasFilter)
{
throw new ServiceException(MessageUtils.message(msgFilter));
@@ -1356,6 +1363,14 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{
query.getParams().put("endBizDate", endBizDate);
}
if (beginPickupDate != null)
{
query.getParams().put("beginPickupDate", beginPickupDate);
}
if (endPickupDate != null)
{
query.getParams().put("endPickupDate", endPickupDate);
}
}
List<PrcOrderBill> list = self.selectBillList(query);
return list == null ? new ArrayList<PrcOrderBill>() : list;
@@ -2754,6 +2769,10 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
: request.getBeginBizDate().trim();
String endBizDate = StringUtils.isEmpty(request.getEndBizDate()) ? null
: request.getEndBizDate().trim();
String beginPickupDate = StringUtils.isEmpty(request.getBeginPickupDate()) ? null
: request.getBeginPickupDate().trim();
String endPickupDate = StringUtils.isEmpty(request.getEndPickupDate()) ? null
: request.getEndPickupDate().trim();
boolean hasFilter = (orderNos != null && !orderNos.isEmpty())
|| (trackingNos != null && !trackingNos.isEmpty())
|| (transferNos != null && !transferNos.isEmpty())
@@ -2762,7 +2781,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|| (productCodes != null && !productCodes.isEmpty())
|| StringUtils.isNotEmpty(creditArrearsStatus)
|| StringUtils.isNotEmpty(beginBizDate)
|| StringUtils.isNotEmpty(endBizDate);
|| StringUtils.isNotEmpty(endBizDate)
|| StringUtils.isNotEmpty(beginPickupDate)
|| StringUtils.isNotEmpty(endPickupDate);
if (!hasFilter)
{
throw new ServiceException("请先设置列表筛选条件");
@@ -2784,6 +2805,14 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{
query.getParams().put("endBizDate", endBizDate);
}
if (beginPickupDate != null)
{
query.getParams().put("beginPickupDate", beginPickupDate);
}
if (endPickupDate != null)
{
query.getParams().put("endPickupDate", endPickupDate);
}
List<PrcOrderBill> list = self.selectBillList(query);
if (list == null || list.isEmpty())
{
@@ -53,6 +53,11 @@ import com.bms.price.search.PriceChangeEsLog;
import com.bms.price.service.IPrcPriceExcelFileService;
import com.bms.price.service.IPrcPriceSetService;
import com.bms.price.task.PrcPriceSetSaveAsyncRunner;
import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.domain.BmsCustomerPriceSet;
import com.bms.customer.customerinfo.domain.BmsPriceSetOption;
import com.bms.customer.customerinfo.service.IBmsCustomerLogService;
import com.bms.customer.customerinfo.service.IBmsCustomerService;
/**
* 价格套餐 服务层实现
@@ -130,6 +135,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
@Autowired
private IBillSearchStore billSearchStore;
@Autowired
private IBmsCustomerLogService customerLogService;
@Autowired
private IBmsCustomerService bmsCustomerService;
@Override
public PrcPriceSet selectPrcPriceSetById(Long priceSetId)
{
@@ -1582,6 +1593,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
@Transactional(rollbackFor = Exception.class)
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
String taskId, boolean updateOnly, boolean replaceByProductCountry, String countryCode) throws Exception
{
// Async import has no SecurityContext; bind operName for PriceChangeEsLog.operator().
PriceChangeEsLog.beginOperator(operName);
try
{
return importDetailPricesInOperator(priceSetId, psvId, bytes, filename, operName, taskId, updateOnly,
replaceByProductCountry, countryCode);
}
finally
{
PriceChangeEsLog.endOperator();
}
}
private String importDetailPricesInOperator(Long priceSetId, Long psvId, byte[] bytes, String filename,
String operName, String taskId, boolean updateOnly, boolean replaceByProductCountry, String countryCode)
throws Exception
{
PrcPriceSet db = requireSet(priceSetId);
assertChildEditable(db);
@@ -2494,7 +2522,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
try
{
return customerMapper.insertPrcCustomerPriceSet(customer);
int rows = customerMapper.insertPrcCustomerPriceSet(customer);
if (rows > 0)
{
logCustomerPriceSetBind(null, customer, db);
}
return rows;
}
catch (Exception e)
{
@@ -2593,6 +2626,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
validateCustomer(row, db);
customerMapper.updatePrcCustomerPriceSet(row);
logCustomerPriceSetBind(existing, row, db);
replaced++;
}
else
@@ -2613,6 +2647,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
throw e;
}
logCustomerPriceSetBind(null, row, db);
inserted++;
}
}
@@ -2640,12 +2675,30 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
assertChildEditable(db);
customer.setPriceSetId(old.getPriceSetId());
validateCustomer(customer, db);
return customerMapper.updatePrcCustomerPriceSet(customer);
int rows = customerMapper.updatePrcCustomerPriceSet(customer);
if (rows > 0)
{
if (customer.getCustomerId() == null)
{
customer.setCustomerId(old.getCustomerId());
}
if (StringUtils.isEmpty(customer.getCustomerCode()))
{
customer.setCustomerCode(old.getCustomerCode());
}
if (StringUtils.isEmpty(customer.getCustomerName()))
{
customer.setCustomerName(old.getCustomerName());
}
logCustomerPriceSetBind(old, customer, db);
}
return rows;
}
@Override
public int deleteCustomerByIds(Long[] customerPricesetIds)
{
List<PrcCustomerPriceSet> snapshots = new ArrayList<PrcCustomerPriceSet>();
for (Long id : customerPricesetIds)
{
PrcCustomerPriceSet old = customerMapper.selectPrcCustomerPriceSetById(id);
@@ -2658,8 +2711,17 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
throw new ServiceException(MessageUtils.message("price.set.customer.unbindAudit"));
}
snapshots.add(old);
}
return customerMapper.deletePrcCustomerPriceSetByIds(customerPricesetIds);
int rows = customerMapper.deletePrcCustomerPriceSetByIds(customerPricesetIds);
if (rows > 0)
{
for (PrcCustomerPriceSet snapshot : snapshots)
{
logCustomerPriceSetUnbind(snapshot);
}
}
return rows;
}
private void replaceChildren(PrcPriceSet set)
@@ -2730,6 +2792,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
c.setUpdateBy(set.getUpdateBy());
customerMapper.updatePrcCustomerPriceSet(c);
keepIds.add(hit.getCustomerPricesetId());
logCustomerPriceSetBind(hit, c, set);
}
else
{
@@ -2740,6 +2803,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
keepIds.add(c.getCustomerPricesetId());
}
logCustomerPriceSetBind(null, c, set);
}
}
catch (Exception e)
@@ -2761,10 +2825,99 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
if (!orphanIds.isEmpty())
{
for (Long orphanId : orphanIds)
{
PrcCustomerPriceSet snapshot = byId.get(orphanId);
if (snapshot != null)
{
logCustomerPriceSetUnbind(snapshot);
}
}
customerMapper.deletePrcCustomerPriceSetByIds(orphanIds.toArray(new Long[0]));
}
}
/** Write customer biz log for package bind / rebind (visible in customer info Logs). */
private void logCustomerPriceSetBind(PrcCustomerPriceSet existing, PrcCustomerPriceSet bind, PrcPriceSet set)
{
if (customerLogService == null || bind == null || set == null)
{
return;
}
BmsCustomer customer = resolveCustomerForLog(bind);
if (customer == null && bind.getCustomerId() != null)
{
customer = new BmsCustomer();
customer.setCustomerId(bind.getCustomerId());
customer.setCustomerCode(bind.getCustomerCode());
customer.setCustomerName(bind.getCustomerName());
}
BmsCustomerPriceSet existingLog = toBmsCustomerPriceSet(existing, resolvePriceSetName(existing));
BmsCustomerPriceSet bindLog = toBmsCustomerPriceSet(bind, set.getPriceSetName());
if (bindLog.getCustomerId() == null && customer != null)
{
bindLog.setCustomerId(customer.getCustomerId());
}
BmsPriceSetOption option = new BmsPriceSetOption();
option.setPriceSetId(set.getPriceSetId());
option.setPriceSetCode(set.getPriceSetCode());
option.setPriceSetName(set.getPriceSetName());
customerLogService.recordPriceSetBind(customer, existingLog, bindLog, option);
}
private void logCustomerPriceSetUnbind(PrcCustomerPriceSet bind)
{
if (customerLogService == null || bind == null)
{
return;
}
BmsCustomerPriceSet snapshot = toBmsCustomerPriceSet(bind, resolvePriceSetName(bind));
customerLogService.recordPriceSetUnbind(snapshot);
}
private BmsCustomer resolveCustomerForLog(PrcCustomerPriceSet bind)
{
if (bind == null || bmsCustomerService == null)
{
return null;
}
if (bind.getCustomerId() != null)
{
return bmsCustomerService.selectBmsCustomerByCustomerId(bind.getCustomerId());
}
return null;
}
private String resolvePriceSetName(PrcCustomerPriceSet bind)
{
if (bind == null || bind.getPriceSetId() == null)
{
return null;
}
PrcPriceSet set = setMapper.selectPrcPriceSetById(bind.getPriceSetId());
return set == null ? null : set.getPriceSetName();
}
private BmsCustomerPriceSet toBmsCustomerPriceSet(PrcCustomerPriceSet src, String priceSetName)
{
if (src == null)
{
return null;
}
BmsCustomerPriceSet dest = new BmsCustomerPriceSet();
dest.setCustomerPricesetId(src.getCustomerPricesetId());
dest.setPriceSetId(src.getPriceSetId());
dest.setCustomerId(src.getCustomerId());
dest.setCustomerCode(src.getCustomerCode());
dest.setCustomerName(src.getCustomerName());
dest.setBindPriority(src.getBindPriority());
dest.setPrBegindate(src.getPrBegindate());
dest.setPrEnddate(src.getPrEnddate());
dest.setPriceSetNote(src.getPriceSetNote());
dest.setPriceSetName(priceSetName);
return dest;
}
private void reportSaveProgress(PrcPriceSet set, int percent, String message)
{
if (set == null || StringUtils.isEmpty(set.getSaveTaskId()) || saveAsyncRunner == null)
@@ -3465,6 +3618,22 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
fillVersionChildren(version);
}
// Soft-deleted EFFECT versions for the "deleted" tab (status may still be EFFECT)
PrcPriceSetVersion dq = new PrcPriceSetVersion();
dq.setPriceSetId(priceSetId);
dq.setPriceType(com.bms.price.domain.PriceType.SALES);
dq.setIsDel("1");
List<PrcPriceSetVersion> deleted = versionMapper.selectPrcPriceSetVersionList(dq);
if (deleted != null)
{
for (PrcPriceSetVersion version : deleted)
{
version.setProducts(new ArrayList<PrcPriceSetProduct>());
version.setDetails(new ArrayList<PrcPriceSetDetail>());
version.setDetailCount(Integer.valueOf(detailMapper.countByPsvIdAll(version.getPsvId())));
versions.add(version);
}
}
set.setVersions(versions);
PrcPriceSetVersion current = pickCurrentVersion(versions);
set.setVersion(current);
@@ -3491,14 +3660,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
return null;
}
PrcPriceSetVersion firstAlive = null;
for (PrcPriceSetVersion v : versions)
{
if (v == null || "1".equals(v.getIsDel()))
{
continue;
}
if (firstAlive == null)
{
firstAlive = v;
}
if (VER_EFFECT.equals(v.getStatus()))
{
return v;
}
}
return versions.get(0);
return firstAlive;
}
private void saveOrUpdateVersion(PrcPriceSet set)
@@ -0,0 +1,382 @@
package com.bms.price.service.impl;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson2.JSON;
import com.bms.common.core.domain.entity.SysDictData;
import com.bms.common.core.domain.entity.SysUser;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.DateUtils;
import com.bms.common.utils.PageUtils;
import com.bms.common.utils.SecurityUtils;
import com.bms.price.airbill.domain.SrvBillAirFee;
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
import com.bms.price.airbill.domain.SrvBillAirFeeSave;
import com.bms.price.mapper.SrvBillAirFeeMapper;
import com.bms.price.mapper.SrvBillAirMapper;
import com.bms.price.service.ISrvBillAirFeeService;
import com.bms.system.service.ISysDictTypeService;
/**
* 提单费用只处理本次新增、修改、删除的行。没有变化的费用不更新。
*/
@Service
public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
{
private static final String FEE_DICT = "bms_fee_type";
private static final String CURRENCY_DICT = "bms_currency";
@Autowired
private SrvBillAirFeeMapper feeMapper;
@Autowired
private SrvBillAirMapper billMapper;
@Autowired
private ISysDictTypeService dictTypeService;
@Override
public List<SrvBillAirFee> listFees(String billCode)
{
requireBill(billCode);
return feeMapper.selectByBillCode(billCode.trim());
}
@Override
public List<SrvBillAirFeeLog> listLogs(String billCode)
{
requireBill(billCode);
PageUtils.startPage();
List<SrvBillAirFeeLog> logs = feeMapper.selectLogs(billCode.trim());
for (SrvBillAirFeeLog log : logs)
{
if (log.getChangeContent() != null && !log.getChangeContent().isBlank())
{
log.setChanges(JSON.parseArray(log.getChangeContent(), SrvBillAirFeeLog.FieldChange.class));
}
}
return logs;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String save(SrvBillAirFeeSave command)
{
if (command == null)
{
throw new ServiceException("请提交要保存的费用");
}
String billCode = requireBill(command.getBillCode());
List<SrvBillAirFee> inserts = command.getInserts() == null ? List.of() : command.getInserts();
List<SrvBillAirFee> updates = command.getUpdates() == null ? List.of() : command.getUpdates();
List<Long> deletes = command.getDeletes() == null ? List.of() : command.getDeletes();
if (inserts.isEmpty() && updates.isEmpty() && deletes.isEmpty())
{
throw new ServiceException("没有需要保存的变更");
}
Map<String, String> feeLabels = dictLabels(FEE_DICT, "费用类型");
Map<String, String> currencyLabels = dictLabels(CURRENCY_DICT, "币种");
Operator operator = operator();
Date now = DateUtils.getNowDate();
Set<Long> seen = new HashSet<>();
int inserted = 0;
int updated = 0;
int removed = 0;
for (Long id : deletes)
{
if (id == null)
{
throw new ServiceException("要删除的费用缺少编号");
}
if (!seen.add(id))
{
throw new ServiceException("同一条费用不能重复提交");
}
SrvBillAirFee current = liveFee(id, billCode);
if (current == null)
{
throw new ServiceException("费用已删除或不属于该提单");
}
int rows = feeMapper.deleteFee(current.getId(), billCode, operator.account, now);
if (rows > 0)
{
writeLog(billCode, id, SrvBillAirFeeLog.DELETE, current, null, feeLabels, currencyLabels, operator, now);
removed++;
}
}
for (SrvBillAirFee incoming : updates)
{
if (incoming == null || incoming.getId() == null)
{
throw new ServiceException("要修改的费用缺少编号");
}
if (!seen.add(incoming.getId()))
{
throw new ServiceException("同一条费用不能重复提交");
}
SrvBillAirFee current = liveFee(incoming.getId(), billCode);
if (current == null)
{
throw new ServiceException("费用已删除或不属于该提单");
}
SrvBillAirFee next = normalize(incoming, feeLabels, currencyLabels);
List<SrvBillAirFeeLog.FieldChange> changes = diff(current, next, feeLabels, currencyLabels);
if (changes.isEmpty())
{
continue;
}
next.setId(current.getId());
next.setBillCode(billCode);
next.setUpdateBy(operator.account);
next.setUpdateTime(now);
if (feeMapper.updateFee(next) > 0)
{
writeLog(billCode, current.getId(), SrvBillAirFeeLog.UPDATE, current, next, feeLabels, currencyLabels, operator, now);
updated++;
}
}
for (SrvBillAirFee incoming : inserts)
{
if (incoming == null || blank(incoming.getFeeType()) && incoming.getPrice() == null && blank(incoming.getCurrency()) && blank(incoming.getRemark()))
{
continue;
}
SrvBillAirFee next = normalize(incoming, feeLabels, currencyLabels);
next.setId(null);
next.setBillCode(billCode);
next.setCreateBy(operator.account);
next.setCreateTime(now);
next.setUpdateBy(operator.account);
next.setUpdateTime(now);
feeMapper.insertFee(next);
writeLog(billCode, next.getId(), SrvBillAirFeeLog.INSERT, null, next, feeLabels, currencyLabels, operator, now);
inserted++;
}
if (inserted + updated + removed == 0)
{
throw new ServiceException("没有需要保存的变更");
}
return "已保存:新增 " + inserted + " 条,修改 " + updated + " 条,删除 " + removed + " 条";
}
private SrvBillAirFee liveFee(Long id, String billCode)
{
return feeMapper.selectLive(id, billCode);
}
private SrvBillAirFee normalize(SrvBillAirFee raw, Map<String, String> feeLabels, Map<String, String> currencyLabels)
{
String feeType = trim(raw.getFeeType());
String currency = trim(raw.getCurrency());
String remark = trim(raw.getRemark());
if (feeType.isEmpty())
{
throw new ServiceException("请选择费用项");
}
if (feeType.length() > 32 || !feeLabels.containsKey(feeType))
{
throw new ServiceException("费用项不在费用类型字典中");
}
if (raw.getPrice() == null)
{
throw new ServiceException("请填写价格");
}
if (raw.getPrice().signum() < 0)
{
throw new ServiceException("价格不能小于0");
}
if (raw.getPrice().precision() - raw.getPrice().scale() > 16)
{
throw new ServiceException("价格整数部分不能超过16位");
}
if (currency.isEmpty())
{
throw new ServiceException("请选择收费币种");
}
if (currency.length() > 16 || !currencyLabels.containsKey(currency))
{
throw new ServiceException("收费币种不在币种字典中");
}
if (remark.length() > 500)
{
throw new ServiceException("备注不能超过500字");
}
SrvBillAirFee fee = new SrvBillAirFee();
fee.setFeeType(feeType);
fee.setPrice(raw.getPrice());
fee.setCurrency(currency);
fee.setRemark(remark);
return fee;
}
private void writeLog(String billCode, Long feeId, String operation, SrvBillAirFee before, SrvBillAirFee after,
Map<String, String> feeLabels, Map<String, String> currencyLabels, Operator operator, Date now)
{
List<SrvBillAirFeeLog.FieldChange> changes = diff(before, after, feeLabels, currencyLabels);
SrvBillAirFeeLog log = new SrvBillAirFeeLog();
log.setBillCode(billCode);
log.setFeeId(feeId);
log.setOperationType(operation);
log.setSummary(cut(summary(operation, changes), 1000));
log.setChangeContent(JSON.toJSONString(changes));
log.setCreateBy(operator.account);
log.setCreateByName(operator.name);
log.setCreateTime(now);
feeMapper.insertLog(log);
}
private static List<SrvBillAirFeeLog.FieldChange> diff(SrvBillAirFee before, SrvBillAirFee after,
Map<String, String> feeLabels, Map<String, String> currencyLabels)
{
List<SrvBillAirFeeLog.FieldChange> changes = new ArrayList<>();
addChange(changes, "feeType", "费用类型", label(feeLabels, value(before, SrvBillAirFee::getFeeType)),
label(feeLabels, value(after, SrvBillAirFee::getFeeType)));
addChange(changes, "price", "价格", priceText(before), priceText(after));
addChange(changes, "currency", "收费币种", label(currencyLabels, value(before, SrvBillAirFee::getCurrency)),
label(currencyLabels, value(after, SrvBillAirFee::getCurrency)));
addChange(changes, "remark", "备注", text(before == null ? null : before.getRemark()), text(after == null ? null : after.getRemark()));
return changes;
}
private static void addChange(List<SrvBillAirFeeLog.FieldChange> changes, String field, String fieldLabel, String oldValue, String newValue)
{
String left = oldValue == null ? "" : oldValue;
String right = newValue == null ? "" : newValue;
if (left.equals(right))
{
return;
}
changes.add(new SrvBillAirFeeLog.FieldChange(field, fieldLabel, left, right));
}
private static String summary(String operation, List<SrvBillAirFeeLog.FieldChange> changes)
{
String action = SrvBillAirFeeLog.INSERT.equals(operation) ? "新增费用"
: SrvBillAirFeeLog.DELETE.equals(operation) ? "删除费用" : "修改费用";
StringBuilder text = new StringBuilder(action);
for (SrvBillAirFeeLog.FieldChange change : changes)
{
text.append(";").append(change.getFieldLabel()).append(":")
.append(show(change.getOldValue())).append(" → ").append(show(change.getNewValue()));
}
return text.toString();
}
private Map<String, String> dictLabels(String dictType, String name)
{
List<SysDictData> rows = dictTypeService.selectDictDataByType(dictType);
Map<String, String> labels = new LinkedHashMap<>();
if (rows != null)
{
for (SysDictData row : rows)
{
if (row != null && row.getDictValue() != null && !row.getDictValue().isBlank())
{
labels.put(row.getDictValue().trim(), row.getDictLabel() == null ? row.getDictValue().trim() : row.getDictLabel().trim());
}
}
}
if (labels.isEmpty())
{
throw new ServiceException(name + "字典还没有数据");
}
return labels;
}
private String requireBill(String billCode)
{
if (billCode == null || billCode.isBlank())
{
throw new ServiceException("提单号不能为空");
}
String code = billCode.trim();
if (billMapper.selectSrvBillAirByBillCode(code) == null)
{
throw new ServiceException("提单不存在");
}
return code;
}
private static Operator operator()
{
SysUser user = SecurityUtils.getLoginUser().getUser();
String account = user.getUserName() == null ? "" : user.getUserName();
String name = user.getNickName() == null || user.getNickName().isBlank() ? account : user.getNickName();
return new Operator(account, name);
}
private static String value(SrvBillAirFee fee, java.util.function.Function<SrvBillAirFee, String> getter)
{
return fee == null ? "" : text(getter.apply(fee));
}
private static String priceText(SrvBillAirFee fee)
{
if (fee == null || fee.getPrice() == null)
{
return "";
}
return fee.getPrice().stripTrailingZeros().toPlainString();
}
private static String label(Map<String, String> labels, String value)
{
if (value == null || value.isBlank())
{
return "";
}
return labels.getOrDefault(value, value);
}
private static String show(String value)
{
return value == null || value.isBlank() ? "空" : value;
}
private static String text(String value)
{
return value == null ? "" : value.trim();
}
private static String trim(String value)
{
return value == null ? "" : value.trim();
}
private static boolean blank(String value)
{
return value == null || value.trim().isEmpty();
}
private static String cut(String value, int max)
{
if (value == null || value.length() <= max)
{
return value;
}
return value.substring(0, max);
}
private static final class Operator
{
private final String account;
private final String name;
private Operator(String account, String name)
{
this.account = account;
this.name = name;
}
}
}
@@ -36,8 +36,7 @@ import com.bms.price.service.IPrcOrderBillService;
/**
* 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.
* Continuous worker pull (no page barrier); cancel skips remaining billIds.
*/
@Component
public class PrcOrderBillLocalRebillAsyncRunner
@@ -46,13 +45,11 @@ public class PrcOrderBillLocalRebillAsyncRunner
public static final String CACHE_KEY_PREFIX = "price:billing:localRebill:task:";
public static final int TTL_MINUTES = 120;
/** Bill this many, wait for page done, then next page. */
public static final int PAGE_SIZE = 500;
public static final int MIN_THREADS = 2;
public static final int MAX_THREADS = 16;
public static final int MAX_THREADS = 24;
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;
/** Publish Redis progress at most this often while workers run. */
private static final int PROGRESS_EVERY_N = 50;
private static final long PROGRESS_MIN_INTERVAL_MS = 500L;
private final ConcurrentHashMap<String, AtomicBoolean> cancelFlags = new ConcurrentHashMap<String, AtomicBoolean>();
@@ -86,10 +83,12 @@ public class PrcOrderBillLocalRebillAsyncRunner
AtomicInteger success = new AtomicInteger(0);
AtomicInteger fail = new AtomicInteger(0);
AtomicInteger done = new AtomicInteger(0);
AtomicInteger nextIndex = 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());
int configured = billingProperties == null ? 16 : billingProperties.getLocalRebillThreads();
int threads = clampThreads(Integer.valueOf(configured));
cancelFlags.computeIfAbsent(taskId, k -> new AtomicBoolean(false));
ExecutorService pool = newWorkerPool(threads, "local-rebill");
try
@@ -107,30 +106,24 @@ public class PrcOrderBillLocalRebillAsyncRunner
finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails);
return;
}
boolean cancelled = false;
for (int from = 0; from < total; from += PAGE_SIZE)
// Continuous pull: N workers grab next billId — no page barrier idle wait.
final List<String> ids = billIds;
List<Callable<Boolean>> workers = new ArrayList<Callable<Boolean>>(threads);
for (int w = 0; w < threads; w++)
{
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());
for (String billId : page)
{
final String one = billId;
jobs.add(() -> {
if (isCancelRequested(taskId))
workers.add(() -> {
while (!isCancelRequested(taskId))
{
int i = nextIndex.getAndIncrement();
if (i >= total)
{
return Boolean.FALSE;
return Boolean.TRUE;
}
String one = ids.get(i);
try
{
orderBillService.rebillLocalByBillId(one);
success.incrementAndGet();
return Boolean.TRUE;
}
catch (Exception e)
{
@@ -139,26 +132,20 @@ public class PrcOrderBillLocalRebillAsyncRunner
collectFailDetail(failDetails, one, e);
log.warn("local rebill failed taskId={} billId={} msg={}",
taskId, one, e.getMessage());
return Boolean.FALSE;
}
finally
{
int finished = done.incrementAndGet();
maybePublishProgress(taskId, total, finished, success.get(), fail.get(), lastProgressAt);
maybePublishProgress(taskId, total, finished, success.get(), fail.get(),
lastProgressAt);
}
});
}
invokeAllQuiet(pool, jobs);
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());
}
return Boolean.FALSE;
});
}
if (cancelled || isCancelRequested(taskId))
invokeAllQuiet(pool, workers);
publishProgress(taskId, total, done.get(), success.get(), fail.get());
if (isCancelRequested(taskId))
{
finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails);
return;
@@ -170,8 +157,8 @@ public class PrcOrderBillLocalRebillAsyncRunner
String doneStatus = fail.get() > 0 ? EsBizLogSupport.STATUS_FAIL : EsBizLogSupport.STATUS_SUCCESS;
String doneErr = fail.get() > 0 ? ("fail=" + fail.get() + ", ok=" + success.get()) : null;
saveDoneLog(taskId, request, total, success.get(), fail.get(), threads, failDetails, doneStatus, doneErr);
log.info("local rebill done taskId={} total={} success={} fail={} threads={} pageSize={}",
taskId, total, success.get(), fail.get(), threads, PAGE_SIZE);
log.info("local rebill done taskId={} total={} success={} fail={} threads={}",
taskId, total, success.get(), fail.get(), threads);
}
catch (Exception e)
{
@@ -255,7 +242,6 @@ public class PrcOrderBillLocalRebillAsyncRunner
resp.put("fail", Integer.valueOf(fail));
resp.put("success", Integer.valueOf(ok));
resp.put("threads", Integer.valueOf(threads));
resp.put("pageSize", Integer.valueOf(PAGE_SIZE));
if (failDetails != null && !failDetails.isEmpty())
{
resp.put("failDetails", new ArrayList<Map<String, Object>>(failDetails));
@@ -66,14 +66,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="query.endTime != null and query.endTime != ''">
AND o.PICKUP_DATE &lt;= TO_DATE(#{query.endTime}, 'YYYY-MM-DD HH24:MI:SS')
</if>
<choose>
<when test="query.blPickup != null">
AND o.BL_PICKUP = #{query.blPickup}
</when>
<!-- <otherwise>-->
<!-- AND o.BL_PICKUP = 1-->
<!-- </otherwise>-->
</choose>
AND o.BL_PICKUP = 1
<if test="query.orderStatuses != null and query.orderStatuses.size() > 0">
AND o.ORDER_STATUS IN
<foreach collection="query.orderStatuses" item="st" open="(" separator="," close=")">
@@ -281,14 +274,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<foreach collection="batch" item="bid" open="(" separator="," close=")">
#{bid}
</foreach>
<choose>
<when test="blPickup != null">
AND o.BL_PICKUP = #{blPickup}
</when>
<!-- <otherwise>-->
<!-- AND o.BL_PICKUP = 1-->
<!-- </otherwise>-->
</choose>
AND o.BL_PICKUP = 1
AND o.ORDER_STATUS IN (3, 4, 5, 6, 7, 8, 11)
<if test="customerCodes != null and customerCodes.size() > 0">
AND o.CUSTOMER_CODE IN
@@ -158,6 +158,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select count(1) from prc_price_set_detail where psv_id = #{psvId} and ifnull(is_del, '0') = '0'
</select>
<select id="countByPsvIdAll" parameterType="Long" resultType="int">
select count(1) from prc_price_set_detail where psv_id = #{psvId}
</select>
<select id="selectDetailOptionRows" resultMap="PrcPriceSetDetailResult">
select distinct product_code, product_name, country_code, country_name, zone_code, zone_name
from prc_price_set_detail
@@ -37,7 +37,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectPrcPriceSetVersionList" parameterType="PrcPriceSetVersion" resultMap="PrcPriceSetVersionResult">
<include refid="selectPrcPriceSetVersionVo"/>
<where>
and ifnull(is_del, '0') = '0'
<choose>
<when test="isDel != null and isDel != ''">
and ifnull(is_del, '0') = #{isDel}
</when>
<otherwise>
and ifnull(is_del, '0') = '0'
</otherwise>
</choose>
<if test="priceSetId != null">
AND price_set_id = #{priceSetId}
</if>
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bms.price.mapper.SrvBillAirFeeMapper">
<select id="selectByBillCode" resultType="com.bms.price.airbill.domain.SrvBillAirFee">
select id, bill_code as billCode, fee_type as feeType, price, currency, remark,
create_by as createBy, create_time as createTime, update_by as updateBy,
update_time as updateTime, del_flag as delFlag
from srv_bill_air_fee
where bill_code = #{billCode}
and del_flag = '0'
order by id
</select>
<select id="selectLive" resultType="com.bms.price.airbill.domain.SrvBillAirFee">
select id, bill_code as billCode, fee_type as feeType, price, currency, remark,
create_by as createBy, create_time as createTime, update_by as updateBy,
update_time as updateTime, del_flag as delFlag
from srv_bill_air_fee
where id = #{id}
and bill_code = #{billCode}
and del_flag = '0'
</select>
<insert id="insertFee" useGeneratedKeys="true" keyProperty="id">
insert into srv_bill_air_fee (
bill_code, fee_type, price, currency, remark,
create_by, create_time, update_by, update_time, del_flag
) values (
#{billCode}, #{feeType}, #{price}, #{currency}, #{remark},
#{createBy}, #{createTime}, #{updateBy}, #{updateTime}, '0'
)
</insert>
<update id="updateFee">
update srv_bill_air_fee
set fee_type = #{feeType},
price = #{price},
currency = #{currency},
remark = #{remark},
update_by = #{updateBy},
update_time = #{updateTime}
where id = #{id}
and bill_code = #{billCode}
and del_flag = '0'
</update>
<update id="deleteFee">
update srv_bill_air_fee
set del_flag = '1',
update_by = #{updateBy},
update_time = #{updateTime}
where id = #{id}
and bill_code = #{billCode}
and del_flag = '0'
</update>
<insert id="insertLog" useGeneratedKeys="true" keyProperty="logId">
insert into srv_bill_air_fee_log (
bill_code, fee_id, operation_type, summary, change_content,
create_by, create_by_name, create_time
) values (
#{billCode}, #{feeId}, #{operationType}, #{summary}, #{changeContent},
#{createBy}, #{createByName}, #{createTime}
)
</insert>
<select id="selectLogs" resultType="com.bms.price.airbill.domain.SrvBillAirFeeLog">
select log_id as logId, bill_code as billCode, fee_id as feeId, operation_type as operationType,
summary, change_content as changeContent, create_by as createBy,
create_by_name as createByName, create_time as createTime
from srv_bill_air_fee_log
where bill_code = #{billCode}
order by log_id desc
</select>
</mapper>