This commit is contained in:
luojiaquan
2026-10-23 15:57:15 +08:00
14 changed files with 417 additions and 71 deletions
@@ -31,6 +31,8 @@ public class BatchAddIncomeBillFeeRequest implements Serializable
private List<String> productCodes;
private String countryCode;
private String creditArrearsStatus;
/** Same as list filter billing_fail_status IN (...). */
private List<Integer> billingFailStatuses;
/** Billing time range (biz_date), inclusive; format yyyy-MM-dd. */
private String beginBizDate;
@@ -25,6 +25,8 @@ public class RecalcBillPriceRequest implements Serializable
private List<String> productCodes;
private String countryCode;
private String creditArrearsStatus;
/** Same as list filter billing_fail_status IN (...). */
private List<Integer> billingFailStatuses;
private String beginBizDate;
private String endBizDate;
}
@@ -38,6 +38,11 @@ public class TabOrderMonitorQuery implements Serializable
private List<String> orderIdentifiers;
/** 上游产品代码 LINE_CODE(产品比对点进订单号时使用) */
private String productCode;
/**
* 订单状态 ORDER_STATUS 多选。
* 空则服务端回落默认 3,4,5,6,7,8,11(与历史硬编码一致)。
*/
private List<Integer> orderStatuses;
/** 分批同步计费每批条数 */
private Integer batchSize;
}
@@ -0,0 +1,49 @@
package com.bms.price.enums;
/**
* Snapshot of prc_order_bill.order_status (aligned with upstream TAB_ORDER).
* Fee refund sets {@link #REFUNDED} (9 / 已退件).
*/
public enum BillOrderStatus
{
/** Upstream: returned package; also set after billing-list fee refund. */
REFUNDED(9);
private final int code;
BillOrderStatus(int code)
{
this.code = code;
}
public int getCode()
{
return code;
}
public Integer getCodeBoxed()
{
return Integer.valueOf(code);
}
public boolean matches(Integer status)
{
return status != null && status.intValue() == code;
}
public static BillOrderStatus of(Integer status)
{
if (status == null)
{
return null;
}
for (BillOrderStatus value : values())
{
if (value.code == status.intValue())
{
return value;
}
}
return null;
}
}
@@ -3,6 +3,7 @@ package com.bms.price.oracle;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
@@ -71,6 +72,12 @@ public class TabOrderMonitorService
/** 单批 Oracle 查询最长等待(从库 socketTimeout 为 120s,留同样余量)。 */
private static final long COLLECT_BATCH_TIMEOUT_SECONDS = 120L;
/** Same default as historical hardcoded ORDER_STATUS IN (...). */
private static final List<Integer> DEFAULT_ORDER_STATUSES =
Collections.unmodifiableList(Arrays.asList(
Integer.valueOf(3), Integer.valueOf(4), Integer.valueOf(5), Integer.valueOf(6),
Integer.valueOf(7), Integer.valueOf(8), Integer.valueOf(11)));
@Autowired
private OracleUpstreamOrderMapper oracleUpstreamOrderMapper;
@@ -994,7 +1001,8 @@ public class TabOrderMonitorService
appendSorted(sb, q.getCustomerCodes());
sb.append('|').append(safe(q.getBeginTime())).append('|').append(safe(q.getEndTime()))
.append('|').append(safe(q.getOrderId())).append('|').append(safe(q.getOrgOrderId()))
.append('|').append(safe(q.getTransferOrderId()));
.append('|').append(safe(q.getTransferOrderId())).append('|');
appendSortedInts(sb, q.getOrderStatuses());
return Integer.toHexString(sb.toString().hashCode());
}
@@ -1012,6 +1020,20 @@ public class TabOrderMonitorService
}
}
private static void appendSortedInts(StringBuilder sb, List<Integer> values)
{
if (values == null || values.isEmpty())
{
return;
}
List<Integer> copy = new ArrayList<Integer>(values);
Collections.sort(copy);
for (Integer v : copy)
{
sb.append(v).append(',');
}
}
private static String safe(String s)
{
return s == null ? "" : s;
@@ -1152,6 +1174,25 @@ public class TabOrderMonitorService
query.setOrderId(trimToNull(query.getOrderId()));
query.setOrgOrderId(trimToNull(query.getOrgOrderId()));
query.setTransferOrderId(trimToNull(query.getTransferOrderId()));
normalizeOrderStatuses(query);
}
/** Empty/missing -> default 3,4,5,6,7,8,11 so SQL always has a status IN list. */
private static void normalizeOrderStatuses(TabOrderMonitorQuery query)
{
List<Integer> statuses = query.getOrderStatuses();
List<Integer> cleaned = new ArrayList<Integer>();
if (statuses != null)
{
for (Integer s : statuses)
{
if (s != null)
{
cleaned.add(s);
}
}
}
query.setOrderStatuses(cleaned.isEmpty() ? new ArrayList<Integer>(DEFAULT_ORDER_STATUSES) : cleaned);
}
/**
@@ -34,6 +34,7 @@ import com.bms.system.service.IBmsProductCodeMappingService;
public class OrderBillProductCheckService
{
private static final int ORDER_DRILL_LIMIT = 500;
private static final int MAX_ORDER_IDENTIFIERS = 1000;
@Autowired
private ITmsPriceQuoteService tmsPriceQuoteService;
@@ -225,6 +226,7 @@ public class OrderBillProductCheckService
throw new ServiceException("please select customer");
}
query.setCustomerCodes(codes);
query.setOrderIdentifiers(normalizeOrderIdentifiers(query.getOrderIdentifiers()));
Map<String, Object> params = query.getParams();
String begin = params == null ? null : strObj(params.get("beginBizDate"));
String end = params == null ? null : strObj(params.get("endBizDate"));
@@ -251,6 +253,34 @@ public class OrderBillProductCheckService
return new ArrayList<String>(set);
}
private static List<String> normalizeOrderIdentifiers(List<String> identifiers)
{
LinkedHashSet<String> unique = new LinkedHashSet<String>();
if (identifiers != null)
{
for (String identifier : identifiers)
{
if (StringUtils.isEmpty(identifier))
{
continue;
}
for (String token : identifier.split("[,,;;\\s]+"))
{
String value = token.trim();
if (StringUtils.isNotEmpty(value))
{
unique.add(value);
}
}
}
}
if (unique.size() > MAX_ORDER_IDENTIFIERS)
{
throw new ServiceException("单号去重后最多输入 " + MAX_ORDER_IDENTIFIERS + " 个");
}
return new ArrayList<String>(unique);
}
private static Date resolveAsOf(PrcOrderBill query)
{
Map<String, Object> params = query.getParams();
@@ -77,6 +77,7 @@ import com.bms.price.domain.vo.TmsPriceQuoteFeeLine;
import com.bms.price.domain.vo.TmsPriceQuoteResult;
import com.bms.price.enums.BillCreditArrearsStatus;
import com.bms.price.enums.BillCustomerActiveStatus;
import com.bms.price.enums.BillOrderStatus;
import com.bms.price.enums.BillRefundStatus;
import com.bms.price.enums.BillBillingFailStatus;
import com.bms.price.enums.TmsOrderBillMatchType;
@@ -106,6 +107,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
private static final String DICT_FEE_TYPE = "bms_fee_type";
private static final String FEE_TYPE_REFUND = "REFUND";
private static final int MAX_ORDER_IDENTIFIERS = 1000;
@Autowired
@Lazy
@@ -299,6 +301,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
refundFee.setManualFlag("0");
incomeBillFeeMapper.insertPrcIncomeBillFee(refundFee);
bill.setRefundStatus(BillRefundStatus.REFUNDED.getCode());
// Align with upstream 已退件 after fee refund.
bill.setOrderStatus(BillOrderStatus.REFUNDED.getCodeBoxed());
if (incomeBill != null)
{
incomeBill.setRefundStatus(BillRefundStatus.REFUNDED.getCode());
@@ -314,6 +318,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
req.put("refundAmount", feeSum.negate());
req.put("feeId", refundFee.getFeeId());
req.put("refundStatus", bill.getRefundStatus());
req.put("orderStatus", bill.getOrderStatus());
if (billSearchStore != null)
{
billSearchStore.save(EsLogType.BILL,
@@ -343,6 +348,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
query.setProductCodes(normalizeCodeList(query.getProductCodes()));
query.setTrackingNos(normalizeCodeList(query.getTrackingNos()));
query.setTransferNos(normalizeCodeList(query.getTransferNos()));
query.setOrderIdentifiers(normalizeOrderIdentifiers(query.getOrderIdentifiers()));
return orderBillMapper.selectPrcOrderBillList(query);
}
@@ -401,6 +407,17 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
return cleaned.isEmpty() ? null : cleaned;
}
/** 单号筛选按去重后的订单号/运单号/转单号数量限制,避免过大的 IN 参数。 */
private List<String> normalizeOrderIdentifiers(List<String> identifiers)
{
List<String> cleaned = normalizeCodeList(identifiers);
if (cleaned != null && cleaned.size() > MAX_ORDER_IDENTIFIERS)
{
throw new ServiceException("单号去重后最多输入 " + MAX_ORDER_IDENTIFIERS + " 个");
}
return cleaned;
}
@Override
public TmsPriceQuoteResult getBill(String billId)
{
@@ -653,7 +670,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
List<PrcOrderBill> bills = resolveListScopedBills(request.getScopeMode(), request.getBillIds(),
request.getOrderNos(), request.getTrackingNos(), request.getTransferNos(), request.getOrderIdentifiers(),
request.getCustomerCodes(), request.getProductCodes(), request.getCountryCode(),
request.getCreditArrearsStatus(), request.getBeginBizDate(), request.getEndBizDate(),
request.getCreditArrearsStatus(), request.getBillingFailStatuses(),
request.getBeginBizDate(), request.getEndBizDate(),
"price.bill.recalc.filterRequired", "price.bill.recalc.scopeRequired",
"price.bill.recalc.selectedRequired");
if (bills.isEmpty())
@@ -748,6 +766,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{
throw new ServiceException(MessageUtils.message("price.bill.bizDateRequired"));
}
assertOrderBillNotRefunded(bill);
TmsPriceBillRequest req = buildBillRequestFromBill(bill);
return billLocalWithDbRetry(billId.trim(), req);
}
@@ -979,7 +998,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
List<String> orderNos = normalizeCodeList(request.getOrderNos());
List<String> trackingNos = normalizeCodeList(request.getTrackingNos());
List<String> transferNos = normalizeCodeList(request.getTransferNos());
List<String> orderIdentifiers = normalizeCodeList(request.getOrderIdentifiers());
List<String> orderIdentifiers = normalizeOrderIdentifiers(request.getOrderIdentifiers());
List<String> customerCodes = normalizeCodeList(request.getCustomerCodes());
List<String> productCodes = normalizeCodeList(request.getProductCodes());
String creditArrearsStatus = StringUtils.isEmpty(request.getCreditArrearsStatus()) ? null
@@ -1030,7 +1049,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
query.setProductCodes(normalizeCodeList(query.getProductCodes()));
query.setTrackingNos(normalizeCodeList(query.getTrackingNos()));
query.setTransferNos(normalizeCodeList(query.getTransferNos()));
query.setOrderIdentifiers(normalizeCodeList(query.getOrderIdentifiers()));
query.setOrderIdentifiers(normalizeOrderIdentifiers(query.getOrderIdentifiers()));
query.setBillIds(normalizeCodeList(query.getBillIds()));
}
@@ -1126,7 +1145,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
return resolveListScopedBills(request.getScopeMode(), request.getBillIds(),
request.getOrderNos(), request.getTrackingNos(), request.getTransferNos(), request.getOrderIdentifiers(),
request.getCustomerCodes(), request.getProductCodes(), request.getCountryCode(),
request.getCreditArrearsStatus(), request.getBeginBizDate(), request.getEndBizDate(),
request.getCreditArrearsStatus(), request.getBillingFailStatuses(),
request.getBeginBizDate(), request.getEndBizDate(),
"price.bill.fee.batch.filterRequired", "price.bill.fee.batch.scopeRequired",
"price.bill.fee.batch.selectedRequired");
}
@@ -1138,7 +1158,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
private List<PrcOrderBill> resolveListScopedBills(String scopeModeRaw, List<String> billIdsRaw,
List<String> orderNosRaw, List<String> trackingNosRaw, List<String> transferNosRaw, List<String> orderIdentifiersRaw,
List<String> customerCodesRaw, List<String> productCodesRaw, String countryCodeRaw,
String creditArrearsStatusRaw, String beginBizDateRaw, String endBizDateRaw,
String creditArrearsStatusRaw, List<Integer> billingFailStatusesRaw,
String beginBizDateRaw, String endBizDateRaw,
String msgFilter, String msgScope, String msgSelected)
{
String scopeMode = StringUtils.nvl(scopeModeRaw, "").trim().toUpperCase();
@@ -1149,9 +1170,10 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
List<String> orderNos = normalizeCodeList(orderNosRaw);
List<String> trackingNos = normalizeCodeList(trackingNosRaw);
List<String> transferNos = normalizeCodeList(transferNosRaw);
List<String> orderIdentifiers = normalizeCodeList(orderIdentifiersRaw);
List<String> orderIdentifiers = normalizeOrderIdentifiers(orderIdentifiersRaw);
List<String> customerCodes = normalizeCodeList(customerCodesRaw);
List<String> productCodes = normalizeCodeList(productCodesRaw);
List<Integer> billingFailStatuses = normalizeIntegerList(billingFailStatusesRaw);
String creditArrearsStatus = StringUtils.isEmpty(creditArrearsStatusRaw) ? null
: creditArrearsStatusRaw.trim();
String countryCode = StringUtils.isEmpty(countryCodeRaw) ? null
@@ -1164,6 +1186,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|| (orderIdentifiers != null && !orderIdentifiers.isEmpty())
|| (customerCodes != null && !customerCodes.isEmpty())
|| (productCodes != null && !productCodes.isEmpty())
|| (billingFailStatuses != null && !billingFailStatuses.isEmpty())
|| StringUtils.isNotEmpty(creditArrearsStatus)
|| StringUtils.isNotEmpty(countryCode)
|| StringUtils.isNotEmpty(beginBizDate)
@@ -1191,6 +1214,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
query.setCustomerCodes(customerCodes);
query.setProductCodes(productCodes);
query.setCreditArrearsStatus(creditArrearsStatus);
query.setBillingFailStatuses(billingFailStatuses);
query.setCountryCode(countryCode);
if (beginBizDate != null)
{
@@ -1205,6 +1229,23 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
return list == null ? new ArrayList<PrcOrderBill>() : list;
}
private List<Integer> normalizeIntegerList(List<Integer> values)
{
if (values == null || values.isEmpty())
{
return null;
}
List<Integer> out = new ArrayList<Integer>();
for (Integer v : values)
{
if (v != null)
{
out.add(v);
}
}
return out.isEmpty() ? null : out;
}
private PrcSurcharge requireActiveSurcharge(Long scId)
{
PrcSurcharge surcharge = surchargeService.selectPrcSurchargeById(scId);
@@ -1595,6 +1636,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
String billKey = Md5Utils.hash(orderNo);
PrcOrderBill existed = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo);
assertOrderBillEditable(existed);
assertOrderBillNotRefunded(existed);
BigDecimal previousIncome = existed == null ? BigDecimal.ZERO : nz(existed.getIncomeTotal());
PrcOrderBill bill = existed == null ? new PrcOrderBill() : existed;
if (existed == null)
@@ -2051,6 +2093,15 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
}
}
/** Block rebill when refundStatus=REFUNDED (order_status=9 alone is logistics return, not fee refund). */
private void assertOrderBillNotRefunded(PrcOrderBill bill)
{
if (bill != null && BillRefundStatus.REFUNDED.getCode().equalsIgnoreCase(bill.getRefundStatus()))
{
throw new ServiceException("\u8ba2\u5355\u5df2\u9000\u8d39\uff0c\u4e0d\u53ef\u91cd\u65b0\u8ba1\u8d39");
}
}
private static BigDecimal nz(BigDecimal v)
{
return v == null ? BigDecimal.ZERO : v;
@@ -2577,7 +2628,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
boolean hasFilter = (orderNos != null && !orderNos.isEmpty())
|| (trackingNos != null && !trackingNos.isEmpty())
|| (transferNos != null && !transferNos.isEmpty())
|| (request.getOrderIdentifiers() != null && !normalizeCodeList(request.getOrderIdentifiers()).isEmpty())
|| (request.getOrderIdentifiers() != null && !normalizeOrderIdentifiers(request.getOrderIdentifiers()).isEmpty())
|| (customerCodes != null && !customerCodes.isEmpty())
|| (productCodes != null && !productCodes.isEmpty())
|| StringUtils.isNotEmpty(creditArrearsStatus)
@@ -2591,7 +2642,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
query.setOrderNos(orderNos);
query.setTrackingNos(trackingNos);
query.setTransferNos(transferNos);
query.setOrderIdentifiers(normalizeCodeList(request.getOrderIdentifiers()));
query.setOrderIdentifiers(normalizeOrderIdentifiers(request.getOrderIdentifiers()));
query.setCustomerCodes(customerCodes);
query.setProductCodes(productCodes);
query.setCreditArrearsStatus(creditArrearsStatus);
@@ -2895,7 +2946,12 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{
return;
}
if (request.getOrderStatus() != null)
// Fee refund marks order_status=9 (已退件); do not let Oracle sync overwrite it.
if (BillRefundStatus.REFUNDED.getCode().equalsIgnoreCase(bill.getRefundStatus()))
{
bill.setOrderStatus(BillOrderStatus.REFUNDED.getCodeBoxed());
}
else if (request.getOrderStatus() != null)
{
bill.setOrderStatus(request.getOrderStatus());
}
@@ -27,7 +27,9 @@ import com.bms.price.domain.PrcBillingTaskRetryTask;
import com.bms.price.domain.vo.BillingTaskSubmitRequest;
import com.bms.price.domain.vo.TabOrderMonitorQuery;
import com.bms.price.domain.vo.TabOrderMonitorVo;
import com.bms.price.enums.BillOrderStatus;
import com.bms.price.enums.BillTaskSuccessStatus;
import com.bms.price.enums.BillTaskType;
import com.bms.price.oracle.TabOrderMonitorService;
import com.bms.price.search.IBillSearchStore;
import com.bms.price.search.TabOrderSyncEsLog;
@@ -200,7 +202,7 @@ public class TabOrderSyncAsyncRunner
/**
* Sync or refund selected rows.
* @param billType optional PENDING/REBILL/REFUND; null = auto PENDING/REBILL
* @param billType optional PENDING/REBILL/REFUND; null = auto by orderStatus / bill existence
*/
public Map<String, Object> syncRowsParallel(List<TabOrderMonitorVo> rows, String billType)
{
@@ -219,7 +221,7 @@ public class TabOrderSyncAsyncRunner
/**
* Sync or refund selected rows carrying the upstream Oracle full row (skip re-query).
* @param billType optional PENDING/REBILL/REFUND; null = auto PENDING/REBILL
* @param billType optional PENDING/REBILL/REFUND; null = auto by orderStatus / bill existence
*/
public Map<String, Object> syncRowsParallelRows(List<com.bms.price.oracle.TabOrderRow> rows, String billType)
{
@@ -509,6 +511,11 @@ public class TabOrderSyncAsyncRunner
{
req.setBillType(billType.trim());
}
else if (BillOrderStatus.REFUNDED.matches(row.getOrderStatus()))
{
// Upstream 已退件 (ORDER_STATUS=9) -> fee refund task
req.setBillType(BillTaskType.REFUND.getCode());
}
return req;
}
}
@@ -67,7 +67,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND o.PICKUP_DATE &lt;= TO_DATE(#{query.endTime}, 'YYYY-MM-DD HH24:MI:SS')
</if>
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=")">
#{st}
</foreach>
</if>
<if test="query.orderStatuses == null or query.orderStatuses.size() == 0">
AND o.ORDER_STATUS IN (3, 4, 5, 6, 7, 8, 11)
</if>
<if test="query.orderId != null and query.orderId != ''">
AND o.ORDER_ID = #{query.orderId}
</if>
@@ -213,6 +213,7 @@ function submit(skipConfirm) {
orderId: q.orderId || undefined,
orgOrderId: q.orgOrderId || undefined,
transferOrderId: q.transferOrderId || undefined,
orderStatuses: Array.isArray(q.orderStatuses) && q.orderStatuses.length ? q.orderStatuses : undefined,
batchSize: 100
}).then(res => {
const asyncTaskId = res.data?.asyncTaskId || res.data
@@ -18,11 +18,11 @@
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:default-params="defaultParams"
:query="queryList"
row-key="orderId"
:auto-load="false"
selection
search-action-align="center"
:search-per-row="4"
:page-sizes="[100, 200, 300, 400]"
:page-size="100"
@@ -83,6 +83,31 @@
<template #col-country="{ row }">
{{ row.countryName || '-' }} ({{ row.country || '-' }})
</template>
<template #search-orderStatuses="{ model }">
<el-select
v-model="model.orderStatuses"
multiple
clearable
collapse-tags
collapse-tags-tooltip
placeholder="请选择"
style="width: 100%"
>
<template #header>
<el-checkbox
:model-value="isAllOrderStatusSelected(model)"
:indeterminate="isOrderStatusIndeterminate(model)"
@change="(checked) => toggleAllOrderStatus(model, checked)"
>全选</el-checkbox>
</template>
<el-option
v-for="opt in orderStatusOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</template>
<template #col-orderStatus="{ row }">
{{ orderStatusLabel(row.orderStatus) }}
</template>
@@ -130,6 +155,27 @@ const syncFloat = ref(null)
/** 比对回填搜索模式:走专用转单号接口,不影响普通列表 */
const transferFillMode = ref(false)
const orderStatusMap = {
0: '预报订单',
1: '确认订单',
2: '已中转',
3: '已收货',
4: '已装箱',
5: '已出库',
6: '转运中',
7: '已上网',
8: '已签收',
9: '已退件',
10: '已删除',
11: '已复核'
}
const orderStatusOptions = Object.keys(orderStatusMap).map(k => ({ label: orderStatusMap[k], value: Number(k) }))
const defaultParams = {
orderStatuses: [3, 4, 5, 6, 7, 8, 11]
}
const searchColumns = [
{
// 客户名称或客户代码混合录入,一行一个
@@ -164,7 +210,20 @@ const searchColumns = [
clearable: true,
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
}
},
{
label: '订单状态',
prop: 'orderStatuses',
component: 'select',
options: orderStatusOptions,
placeholder: '请选择',
props: {
multiple: true,
clearable: true,
collapseTags: true,
collapseTagsTooltip: true
}
},
]
const columns = [
@@ -186,21 +245,6 @@ const columns = [
// { label: '操作', prop: 'action', align: 'center', width: 120, fixed: 'right', slot: 'action', showInToolbar: false }
]
const orderStatusMap = {
0: '预报订单',
1: '确认订单',
2: '已中转',
3: '已收货',
4: '已装箱',
5: '已出库',
6: '转运中',
7: '已上网',
8: '已签收',
9: '已退件',
10: '已删除',
11: '已复核'
}
const syncFloatVisible = computed(() => {
const p = syncFloat.value
return !!(p && (p.loading || p.finished))
@@ -220,6 +264,30 @@ const syncFloatSummary = computed(() => {
})
const syncFloatTooltip = computed(() => syncFloat.value?.message || '点击查看详情')
const orderStatusAllValues = computed(() => orderStatusOptions.map(o => o.value))
function selectedOrderStatuses(model) {
return Array.isArray(model?.orderStatuses) ? model.orderStatuses : []
}
function isAllOrderStatusSelected(model) {
const selected = selectedOrderStatuses(model)
const all = orderStatusAllValues.value
return all.length > 0 && selected.length === all.length
}
function isOrderStatusIndeterminate(model) {
const selected = selectedOrderStatuses(model)
const all = orderStatusAllValues.value
return selected.length > 0 && selected.length < all.length
}
function toggleAllOrderStatus(model, checked) {
if (!model) return
model.orderStatuses = checked ? [...orderStatusAllValues.value] : []
}
function orderStatusLabel(status) {
if (status === null || status === undefined || status === '') return '-'
const key = Number(status)
@@ -388,7 +456,8 @@ function toSyncRow(row) {
orgOrderId: row.orgOrderId,
transferOrderId: row.transferOrderId,
customerCode: row.customerCode,
pickupDate: row.pickupDate
pickupDate: row.pickupDate,
orderStatus: row.orderStatus
}
}
+52 -18
View File
@@ -14,12 +14,16 @@
virtual
>
<template #search-orderIdentifiers="{ model }">
<multi-value-input
<div class="order-identifier-search">
<el-input
v-model="model.orderIdentifiers"
placeholder="订单号/运单号/转单号"
width="100%"
:search-method="searchOrderIdentifiers"
type="textarea"
:rows="1"
resize="none"
placeholder="请输入订单号、运单号或转单号"
clearable
/>
</div>
</template>
<template #search-customerCodes="{ model }">
<customer-select
@@ -234,7 +238,7 @@
</template>
<script setup name="BillingBill">
import { listBill, getBill, refundBill, listOrderNoOptions, listTrackingNoOptions, listTransferNoOptions, updateCreditArrears, importChargeWeightAsync, downloadChargeWeightTemplate, recalcBillPriceAsync } from '@/api/price/billing'
import { listBill, getBill, refundBill, updateCreditArrears, importChargeWeightAsync, downloadChargeWeightTemplate, recalcBillPriceAsync } from '@/api/price/billing'
import { exportAsyncFromTable } from '@/utils/excelExport'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useProducts } from '@/components/ProductSelect/useProducts'
@@ -244,13 +248,27 @@ import EditChargeWeightDialog from './components/EditChargeWeightDialog.vue'
import ExcelImportDialog from '@/components/ExcelImportDialog'
import ChargeWeightImportAttachDrawer from './components/ChargeWeightImportAttachDrawer.vue'
// 单号下拉:订单号、运单号、转单号合并候选
function searchOrderIdentifiers(keyword) {
return Promise.all([
listOrderNoOptions(keyword).then(res => res.data || []),
listTrackingNoOptions(keyword).then(res => res.data || []),
listTransferNoOptions(keyword).then(res => res.data || [])
]).then(groups => [...new Set(groups.flat().map(v => String(v)))])
const MAX_ORDER_IDENTIFIERS = 1000
function parseOrderIdentifiers(raw) {
const values = Array.isArray(raw) ? raw : [raw]
const identifiers = values
.filter(v => v !== null && v !== undefined)
.flatMap(v => String(v).split(/[\s,,;;]+/))
.map(v => v.trim())
.filter(Boolean)
return [...new Set(identifiers)]
}
function getUniqueOrderIdentifierCount(raw) {
return parseOrderIdentifiers(raw).length
}
function validateOrderIdentifierLimit(raw) {
const count = getUniqueOrderIdentifierCount(raw)
if (count <= MAX_ORDER_IDENTIFIERS) return true
ElMessage.warning(`单号去重后最多输入 ${MAX_ORDER_IDENTIFIERS} 个,当前 ${count} 个`)
return false
}
// 产品下拉选项:复用 ProductSelect 的模块级产品缓存(全应用只拉一次)
@@ -402,8 +420,10 @@ const searchColumns = [
]
function buildBillQuery(params) {
const { bizDateRange, ...rest } = params || {}
const { bizDateRange, orderIdentifiers: rawOrderIdentifiers, ...rest } = params || {}
const query = { ...rest }
const orderIdentifiers = parseOrderIdentifiers(rawOrderIdentifiers)
if (orderIdentifiers.length) query.orderIdentifiers = orderIdentifiers
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
query.params = {
...(query.params || {}),
@@ -415,11 +435,16 @@ function buildBillQuery(params) {
}
function queryList(params) {
if (!validateOrderIdentifierLimit(params?.orderIdentifiers)) {
return Promise.reject(new Error('order identifier limit exceeded'))
}
const query = buildBillQuery(params)
return listBill(query)
}
function handleExport() {
const params = tableRef.value?.getQueryParams?.() || {}
if (!validateOrderIdentifierLimit(params.orderIdentifiers)) return
return exportAsyncFromTable({
baseUrl: '/price/billing/bill/export',
tableRef: tableRef.value,
@@ -491,7 +516,7 @@ function onDetailUpdated(data) {
function hasBatchFeeQuery(q) {
const hasArr = (arr) => Array.isArray(arr) && arr.some(v => v != null && String(v).trim() !== '')
return hasArr(q.orderIdentifiers)
return parseOrderIdentifiers(q.orderIdentifiers).length > 0
|| hasArr(q.orderNos)
|| hasArr(q.trackingNos)
|| hasArr(q.transferNos)
@@ -504,12 +529,16 @@ function hasBatchFeeQuery(q) {
function openBatchFee() {
const q = tableRef.value?.getQueryParams?.() || {}
if (!validateOrderIdentifierLimit(q.orderIdentifiers)) return
const { pageNum, pageSize, bizDateRange, ...rest } = q
if (!hasBatchFeeQuery({ ...rest, bizDateRange })) {
ElMessage.warning('请先设置列表查询条件')
return
}
const listQuery = { ...rest }
const listQuery = {
...rest,
orderIdentifiers: parseOrderIdentifiers(rest.orderIdentifiers)
}
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
listQuery.beginBizDate = bizDateRange[0] || null
listQuery.endBizDate = bizDateRange[1] || null
@@ -525,6 +554,7 @@ function onBatchFeeSuccess() {
function buildRecalcPayload() {
const q = tableRef.value?.getQueryParams?.() || {}
if (!validateOrderIdentifierLimit(q.orderIdentifiers)) return null
const { pageNum, pageSize, bizDateRange, ...rest } = q
if (!hasBatchFeeQuery({ ...rest, bizDateRange })) {
ElMessage.warning('请先设置列表查询条件')
@@ -537,10 +567,11 @@ function buildRecalcPayload() {
orderNos: rest.orderNos || [],
trackingNos: rest.trackingNos || [],
transferNos: rest.transferNos || [],
orderIdentifiers: rest.orderIdentifiers || [],
orderIdentifiers: parseOrderIdentifiers(rest.orderIdentifiers),
customerCodes: rest.customerCodes || [],
productCodes: rest.productCodes || [],
creditArrearsStatus: rest.creditArrearsStatus || undefined
creditArrearsStatus: rest.creditArrearsStatus || undefined,
billingFailStatuses: Array.isArray(rest.billingFailStatuses) ? rest.billingFailStatuses : []
}
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
payload.beginBizDate = bizDateRange[0] || null
@@ -628,7 +659,7 @@ function buildChargeWeightTemplateQuery() {
orderNos: rest.orderNos || [],
trackingNos: rest.trackingNos || [],
transferNos: rest.transferNos || [],
orderIdentifiers: rest.orderIdentifiers || [],
orderIdentifiers: parseOrderIdentifiers(rest.orderIdentifiers),
customerCodes: rest.customerCodes || [],
productCodes: rest.productCodes || [],
creditArrearsStatus: rest.creditArrearsStatus || undefined
@@ -641,6 +672,8 @@ function buildChargeWeightTemplateQuery() {
}
async function downloadChargeWeightImportTemplate() {
const currentQuery = tableRef.value?.getQueryParams?.() || {}
if (cwFillByFilter.value && !validateOrderIdentifierLimit(currentQuery.orderIdentifiers)) return
const payload = buildChargeWeightTemplateQuery()
// Checked: backend fills orderNos from list filters. Unchecked: OSS template CHARGE_WEIGHT.
if (payload.fillByFilter) {
@@ -842,4 +875,5 @@ function refundTagType(status) {
.billing-bill :deep(.basic-form .el-form-item .el-date-editor) {
width: 100%;
}
.order-identifier-search { width: 100%; }
</style>
@@ -211,12 +211,28 @@ function buildQueryTags(q) {
: (q.creditArrearsStatus === 'NORMAL' ? "正常" : q.creditArrearsStatus)
tags.push("额度欠费" + ':' + label)
}
const failStatuses = Array.isArray(q.billingFailStatuses) ? q.billingFailStatuses.filter(v => v != null) : []
if (failStatuses.length) {
const labels = failStatuses.map(v => billingFailStatusLabel(v))
tags.push("计费失败" + ':' + labels.join(', '))
}
if (q.beginBizDate || q.endBizDate) {
tags.push("计费时间:" + (q.beginBizDate || '…') + ' ~ ' + (q.endBizDate || '…'))
}
return tags
}
function billingFailStatusLabel(code) {
const map = {
0: '计费成功',
1: '未匹配产品',
2: '未匹配国家',
3: '未匹配重量段'
}
const key = Number(code)
return map[key] != null ? map[key] : String(code)
}
function loadSurcharges() {
return listSurchargeOptions({ ssCode: 'on' }).then(res => {
surchargeOptions.value = res.data || []
@@ -262,6 +278,7 @@ function buildPayload() {
customerCodes: q.customerCodes || [],
productCodes: q.productCodes || [],
creditArrearsStatus: q.creditArrearsStatus || undefined,
billingFailStatuses: Array.isArray(q.billingFailStatuses) ? q.billingFailStatuses : [],
beginBizDate: q.beginBizDate || undefined,
endBizDate: q.endBizDate || undefined
}
+43 -18
View File
@@ -60,12 +60,16 @@
</el-col>
</template>
<template #search-orderIdentifiers="{ model }">
<multi-value-input
<div class="order-identifier-search">
<el-input
v-model="model.orderIdentifiers"
placeholder="订单号/运单号/转单号"
width="100%"
:search-method="searchOrderIdentifiers"
type="textarea"
:rows="1"
resize="none"
placeholder="请输入订单号、运单号或转单号"
clearable
/>
</div>
</template>
<template #search-customerCodes="{ model }">
<customer-select
@@ -163,7 +167,7 @@
<script setup name="OrderBillQuery">
import { ElMessage, ElMessageBox } from 'element-plus'
import { listOrderBill, listOrderNoOptions, listTrackingNoOptions, listTransferNoOptions, localRebillAsync } from '@/api/price/billing'
import { listOrderBill, localRebillAsync } from '@/api/price/billing'
import { exportAsyncFromTable } from '@/utils/excelExport'
import OrderSourceSyncLogDrawer from '../../monitor/orderSource/components/OrderSourceSyncLogDrawer.vue'
import OrderBillProductCheckDrawer from './components/OrderBillProductCheckDrawer.vue'
@@ -182,6 +186,7 @@ const rebillProgressSuccess = ref(0)
const rebillProgressFail = ref(0)
const tableHeight = ref(480)
let tableResizeObserver = null
const MAX_ORDER_IDENTIFIERS = 1000
const defaultParams = {
orderStatuses: [3, 4, 5, 6, 7, 8, 11],
@@ -330,8 +335,10 @@ const searchColumns = [
]
function applySearchRanges(params) {
const { bizDateRange, pickupDateRange, ...rest } = params
const { bizDateRange, pickupDateRange, orderIdentifiers: rawOrderIdentifiers, ...rest } = params || {}
const query = { ...rest }
const orderIdentifiers = parseOrderIdentifiers(rawOrderIdentifiers)
if (orderIdentifiers.length) query.orderIdentifiers = orderIdentifiers
const rangeParams = {}
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
rangeParams.beginBizDate = bizDateRange[0] || null
@@ -347,11 +354,36 @@ function applySearchRanges(params) {
return query
}
function parseOrderIdentifiers(raw) {
const values = Array.isArray(raw) ? raw : [raw]
const identifiers = values
.filter(v => v !== null && v !== undefined)
.flatMap(v => String(v).split(/[\s,,;;]+/))
.map(v => v.trim())
.filter(Boolean)
return [...new Set(identifiers)]
}
function getUniqueOrderIdentifierCount(raw) {
return parseOrderIdentifiers(raw).length
}
function validateOrderIdentifierLimit(raw) {
const count = getUniqueOrderIdentifierCount(raw)
if (count <= MAX_ORDER_IDENTIFIERS) return true
ElMessage.warning(`单号去重后最多输入 ${MAX_ORDER_IDENTIFIERS} 个,当前 ${count} 个`)
return false
}
function queryList(params) {
return listOrderBill(applySearchRanges(params))
const query = applySearchRanges(params)
if (!validateOrderIdentifierLimit(query.orderIdentifiers)) return Promise.reject(new Error('order identifier limit exceeded'))
return listOrderBill(query)
}
function handleExport() {
const params = tableRef.value?.getQueryParams?.() || {}
if (!validateOrderIdentifierLimit(params.orderIdentifiers)) return
return exportAsyncFromTable({
baseUrl: '/price/billing/orderBill/export',
tableRef: tableRef.value,
@@ -365,9 +397,7 @@ const MAX_REBILL = 10000
function currentIdentifiers() {
const q = tableRef.value?.getQueryParams?.() || {}
return (Array.isArray(q.orderIdentifiers) ? q.orderIdentifiers : [])
.map(v => String(v || '').trim())
.filter(Boolean)
return parseOrderIdentifiers(q.orderIdentifiers)
}
function buildRebillFilter() {
@@ -400,6 +430,7 @@ function buildCheckQuery() {
function handleCheckProducts() {
const q = buildCheckQuery()
if (!validateOrderIdentifierLimit(q.orderIdentifiers)) return
const hasCustomer = Array.isArray(q.customerCodes) && q.customerCodes.length > 0
const begin = q.params?.beginBizDate
const end = q.params?.endBizDate
@@ -474,6 +505,7 @@ async function runLocalRebill(payload, confirmText) {
function openFilterRebill() {
const filter = buildRebillFilter()
const identifiers = filter.orderIdentifiers || []
if (!validateOrderIdentifierLimit(identifiers)) return
if (!hasRebillFilter(filter)) {
ElMessage.warning('请输入单号,或选择客户并选择计费时间后再重新计费;也可勾选订单后点重新计费选中')
return
@@ -514,14 +546,6 @@ function handleBatchRebill(selection) {
)
}
function searchOrderIdentifiers(keyword) {
return Promise.all([
listOrderNoOptions(keyword).then(res => res.data || []),
listTrackingNoOptions(keyword).then(res => res.data || []),
listTransferNoOptions(keyword).then(res => res.data || [])
]).then(groups => [...new Set(groups.flat().map(v => String(v)))])
}
function formatWeight(val) {
if (val == null || val === '') return '-'
const num = Number(val)
@@ -581,6 +605,7 @@ onBeforeUnmount(() => {
.order-bill-query :deep(.basic-form .el-form-item .el-date-editor) {
width: 100%;
}
.order-identifier-search { width: 100%; }
.rebill-progress__msg { margin-top: 12px; color: var(--el-text-color-regular); }
.rebill-progress__text { margin-top: 8px; font-size: 13px; color: var(--el-text-color-secondary); }
.rebill-progress__hint { margin-top: 12px; font-size: 12px; color: var(--el-color-warning); }