“单号”改为多行输入,每行一个,支持最多 2000 个号码。

后端支持换行、空格、逗号、分号分隔。
自动去重、忽略空行。
同时匹配订单号、运单号、转单号:
This commit is contained in:
caojia
2026-09-23 11:28:55 +08:00
parent 67c9d28c8c
commit c92c8c0a8e
6 changed files with 81 additions and 33 deletions
@@ -17,7 +17,7 @@ public class ArBillGenerateQuery
private String currency;
/** 订单号、运单号或转单号关键词,按 OR 条件模糊查询。 */
/** 订单号、运单号或转单号;支持换行、逗号、分号或空格分隔多个号码。 */
private String orderOrTrackingNo;
/** 全选模式下,排除弹窗使用的附加订单号、运单号或转单号关键词。 */
@@ -36,7 +36,7 @@ public class ArBillGenerateRequest
@JsonDeserialize(using = StrictDateDeserializer.class)
private Date billDate;
private String currency = "CNY";
/** 订单号、运单号或转单号关键词,生成时用于再次校验筛选条件。 */
/** 订单号、运单号或转单号原始输入,生成时会拆分并再次校验筛选条件。 */
private String orderOrTrackingNo;
private String bankAccountName;
private String bankAccountNo;
@@ -114,6 +114,8 @@ public class ArBillServiceImpl implements IArBillService {
*/
private static final int EXPLICIT_SOURCE_ID_LIMIT = 10000;
private static final int EXCLUDED_SOURCE_ID_LIMIT = 10000;
/** 单次生成查询最多接收的订单号、运单号或转单号数量。 */
private static final int ORDER_IDENTIFIER_LIMIT = 2000;
/**
* 模板明细行使用的边框、字体和换行样式;样式定义来自 ar_bill_template.xlsx。
*/
@@ -216,13 +218,14 @@ public class ArBillServiceImpl implements IArBillService {
}
/**
* 分页预览可生成订单;订单号和运单号关键字在 SQL 中使用 OR 条件。
* 分页预览可生成订单;多个订单号、运单号或转单号按 OR 条件精确匹配。
*/
@Override
public List<PrcOrderBill> previewBill(ArBillGenerateQuery query) {
validatePreviewQuery(query);
List<String> orderIdentifiers = normalizeOrderIdentifiers(query.getOrderOrTrackingNo());
return sourceBillMapper.selectPrcOrderBillByCustomerPeriod(query.getCustomerId(),
query.getCustomerCode(), StringUtils.trim(query.getOrderOrTrackingNo()),
query.getCustomerCode(), orderIdentifiers,
StringUtils.trim(query.getExcludeOrderOrTrackingNo()), query.getBillPeriodEnd(), query.getCurrency(),
eligibleOrderBillStatuses(), ArIncomeBillFeeStatus.UNBILLED.getCode(), null);
}
@@ -233,8 +236,9 @@ public class ArBillServiceImpl implements IArBillService {
@Override
public BigDecimal sumPreviewBill(ArBillGenerateQuery query) {
validatePreviewQuery(query);
List<String> orderIdentifiers = normalizeOrderIdentifiers(query.getOrderOrTrackingNo());
return nvl(sourceBillMapper.sumPrcOrderBillIncomeByCustomerPeriod(query.getCustomerId(),
query.getCustomerCode(), StringUtils.trim(query.getOrderOrTrackingNo()),
query.getCustomerCode(), orderIdentifiers,
StringUtils.trim(query.getExcludeOrderOrTrackingNo()), query.getBillPeriodEnd(), query.getCurrency(),
eligibleOrderBillStatuses(), ArIncomeBillFeeStatus.UNBILLED.getCode()));
}
@@ -422,7 +426,8 @@ public class ArBillServiceImpl implements IArBillService {
if (task.getTotalCount() == null || task.getTotalCount() < 0) {
log.info("开始统计应收账单生成范围,taskId={}", taskId);
int totalCount = sourceBillMapper.countPrcOrderBillByCustomerPeriod(
request.getCustomerId(), request.getCustomerCode(), StringUtils.trim(request.getOrderOrTrackingNo()),
request.getCustomerId(), request.getCustomerCode(),
normalizeOrderIdentifiers(request.getOrderOrTrackingNo()),
request.getBillPeriodEnd(), request.getCurrency(), eligibleOrderBillStatuses(),
ArIncomeBillFeeStatus.UNBILLED.getCode(),
task.getSnapshotCreateTime(), null, excludedSourceBillIds);
@@ -546,7 +551,8 @@ public class ArBillServiceImpl implements IArBillService {
List<String> explicitIds = selectionMode == ArBillSelectionMode.EXPLICIT
? sourceBillIds : null;
List<PrcOrderBill> sources = sourceBillMapper.selectPrcOrderBillByCustomerPeriodCursor(
request.getCustomerId(), request.getCustomerCode(), StringUtils.trim(request.getOrderOrTrackingNo()),
request.getCustomerId(), request.getCustomerCode(),
normalizeOrderIdentifiers(request.getOrderOrTrackingNo()),
request.getBillPeriodEnd(), StringUtils.isEmpty(request.getCurrency()) ? "CNY" : request.getCurrency(),
eligibleOrderBillStatuses(), ArIncomeBillFeeStatus.UNBILLED.getCode(),
task.getSnapshotCreateTime(), task.getLastSourceBillId(),
@@ -1661,6 +1667,8 @@ public class ArBillServiceImpl implements IArBillService {
if (StringUtils.isNotEmpty(request.getRemark()) && request.getRemark().trim().length() > 500) {
throw new ServiceException("备注不能超过500个字符");
}
// 生成任务会保存原始请求快照,先在入任务前校验号码数量,避免后台任务创建后才失败。
normalizeOrderIdentifiers(request.getOrderOrTrackingNo());
}
private void validatePreviewQuery(ArBillGenerateQuery query) {
@@ -1696,4 +1704,26 @@ public class ArBillServiceImpl implements IArBillService {
}
return new ArrayList<>(uniqueIds);
}
/**
* 将前端多行输入的订单号、运单号和转单号拆分为去重列表。
* 支持换行、空格、英文/中文逗号和英文/中文分号作为分隔符。
*/
private List<String> normalizeOrderIdentifiers(String rawIdentifiers) {
LinkedHashSet<String> uniqueIdentifiers = new LinkedHashSet<>();
if (StringUtils.isEmpty(rawIdentifiers)) {
return new ArrayList<>();
}
String[] identifiers = rawIdentifiers.split("[\\s,,;;]+");
for (String identifier : identifiers) {
String value = StringUtils.trim(identifier);
if (!StringUtils.isEmpty(value)) {
uniqueIdentifiers.add(value);
}
}
if (uniqueIdentifiers.size() > ORDER_IDENTIFIER_LIMIT) {
throw new ServiceException("单号最多输入 " + ORDER_IDENTIFIER_LIMIT + " 个");
}
return new ArrayList<>(uniqueIdentifiers);
}
}
@@ -31,7 +31,7 @@ public interface PrcOrderBillMapper
/** 应收账单生成读取指定客户截止日期前仍存在未出账费用的订单计费快照。 */
public List<PrcOrderBill> selectPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
@Param("customerCode") String customerCode, @Param("orderIdentifiers") List<String> orderIdentifiers,
@Param("excludeOrderOrTrackingNo") String excludeOrderOrTrackingNo,
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
@Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus,
@@ -39,14 +39,14 @@ public interface PrcOrderBillMapper
/** 统计应收账单预览结果金额,供全选模式展示。 */
BigDecimal sumPrcOrderBillIncomeByCustomerPeriod(@Param("customerId") Long customerId,
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
@Param("customerCode") String customerCode, @Param("orderIdentifiers") List<String> orderIdentifiers,
@Param("excludeOrderOrTrackingNo") String excludeOrderOrTrackingNo,
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
@Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus);
/** 异步生成账单按 bill_id 游标读取一批来源订单。 */
List<PrcOrderBill> selectPrcOrderBillByCustomerPeriodCursor(@Param("customerId") Long customerId,
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
@Param("customerCode") String customerCode, @Param("orderIdentifiers") List<String> orderIdentifiers,
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
@Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus,
@Param("snapshotCreateTime") Date snapshotCreateTime,
@@ -54,7 +54,7 @@ public interface PrcOrderBillMapper
@Param("excludedBillIds") List<String> excludedBillIds, @Param("limit") int limit);
int countPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
@Param("customerCode") String customerCode, @Param("orderIdentifiers") List<String> orderIdentifiers,
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
@Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus,
@Param("snapshotCreateTime") Date snapshotCreateTime,
@@ -62,7 +62,7 @@ public interface PrcOrderBillMapper
/** 按生成账单时用户勾选的订单读取,并再次校验账单筛选条件。 */
public List<PrcOrderBill> selectPrcOrderBillByCustomerCutoffAndIds(@Param("customerId") Long customerId,
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
@Param("customerCode") String customerCode, @Param("orderIdentifiers") List<String> orderIdentifiers,
@Param("cutoffDate") Date cutoffDate,
@Param("currency") String currency, @Param("billIds") List<String> billIds,
@Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus);
@@ -267,17 +267,32 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<!-- rename_bill_tables.sql 已将需求文档中的 prc_bill 映射为本项目的 prc_order_bill。 -->
<!-- 生成弹窗预览:按客户、截止日、币种和订单号/运单号/转单号 OR 条件分页查询未入账订单。 -->
<sql id="orderIdentifierCondition">
<if test="orderIdentifiers != null and orderIdentifiers.size() > 0">
and (
p.order_no in
<foreach collection="orderIdentifiers" item="identifier" open="(" separator="," close=")">
#{identifier}
</foreach>
or p.tracking_no in
<foreach collection="orderIdentifiers" item="identifier" open="(" separator="," close=")">
#{identifier}
</foreach>
or p.transfer_no in
<foreach collection="orderIdentifiers" item="identifier" open="(" separator="," close=")">
#{identifier}
</foreach>
)
</if>
</sql>
<select id="selectPrcOrderBillByCustomerPeriod" resultMap="PrcOrderBillResult">
<include refid="selectArBillSourceVo"/>
where p.customer_id = #{customerId}
<if test="customerCode != null and customerCode != ''">
and p.customer_code like concat('%', #{customerCode}, '%')
</if>
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
and (p.order_no like concat('%', #{orderOrTrackingNo}, '%')
or p.tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or p.transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
</if>
<include refid="orderIdentifierCondition"/>
<if test="excludeOrderOrTrackingNo != null and excludeOrderOrTrackingNo != ''">
and (p.order_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or p.tracking_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
@@ -314,11 +329,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="customerCode != null and customerCode != ''">
and p.customer_code like concat('%', #{customerCode}, '%')
</if>
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
and (p.order_no like concat('%', #{orderOrTrackingNo}, '%')
or p.tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or p.transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
</if>
<include refid="orderIdentifierCondition"/>
<if test="excludeOrderOrTrackingNo != null and excludeOrderOrTrackingNo != ''">
and (p.order_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or p.tracking_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
@@ -339,11 +350,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="customerCode != null and customerCode != ''">
and p.customer_code like concat('%', #{customerCode}, '%')
</if>
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
and (p.order_no like concat('%', #{orderOrTrackingNo}, '%')
or p.tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or p.transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
</if>
<include refid="orderIdentifierCondition"/>
and p.biz_date &lt;= #{cutoffDate}
and p.ar_bill_status in
<foreach collection="arBillStatuses" item="status" open="(" separator="," close=")">
@@ -412,11 +419,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="customerCode != null and customerCode != ''">
and p.customer_code like concat('%', #{customerCode}, '%')
</if>
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
and (p.order_no like concat('%', #{orderOrTrackingNo}, '%')
or p.tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or p.transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
</if>
<include refid="orderIdentifierCondition"/>
<if test="currency != null and currency != ''">
and p.currency = #{currency}
</if>
@@ -6,7 +6,14 @@
<el-col :span="8"><el-form-item label="客户" prop="customerId"><customer-select v-model="generateForm.customerId" placeholder="请选择客户" @change="handleCustomerChange" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="币种" prop="currency"><el-select v-model="generateForm.currency" placeholder="请选择币种" style="width: 100%;" @change="handleCurrencyChange"><el-option label="人民币 CNY" value="CNY" /><el-option label="美元 USD" value="USD" /></el-select></el-form-item></el-col>
<el-col :span="8"><el-form-item label="账单截止日" prop="cutoffDate"><el-date-picker v-model="generateForm.cutoffDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择账单截止日" style="width: 100%;" @change="resetPreview" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="单号"><el-input v-model="generateForm.orderOrTrackingNo" placeholder="请输入订单号、运单号或转单号" clearable /></el-form-item></el-col>
<el-col :span="8">
<el-form-item label="单号">
<div class="order-identifier-input">
<el-input v-model="generateForm.orderOrTrackingNo" type="textarea" :rows="4" resize="vertical" placeholder="请输入订单号、运单号或转单号,每行一个,最多2000个" clearable />
<el-button v-if="generateForm.orderOrTrackingNo" class="order-identifier-clear" link type="primary" @click="clearOrderIdentifiers">清空</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="payee-card" v-loading="bankAccountLoading">
@@ -231,6 +238,11 @@ function handleCurrencyChange() {
loadDefaultBankAccount()
}
// 只清空输入内容,不自动刷新预览;用户点击“查询”后再按新条件查询。
function clearOrderIdentifiers() {
generateForm.orderOrTrackingNo = ''
}
function loadDefaultBankAccount() {
bankAccountLoading.value = true
bankAccount.value = null
@@ -520,6 +532,9 @@ function createRequestId() { return globalThis.crypto?.randomUUID?.() || `ar-${D
.generate-dialog :deep(.el-dialog__body) { padding: 18px 24px 10px; }
.generate-form { padding: 4px 6px 0; border: 1px solid var(--el-border-color-lighter); border-radius: 8px; background: var(--el-fill-color-blank); }
.generate-form :deep(.el-form-item) { margin-bottom: 16px; }
.order-identifier-input { position: relative; width: 100%; }
.order-identifier-input :deep(.el-textarea__inner) { padding-right: 48px; }
.order-identifier-clear { position: absolute; top: 4px; right: 6px; z-index: 1; padding: 2px 4px; }
.payee-card { display: flex; align-items: center; flex-wrap: wrap; gap: 16px; min-height: 42px; margin: 4px 0 12px; padding: 9px 12px; border: 1px solid var(--el-border-color-lighter); border-radius: 4px; background: var(--el-fill-color-light); color: var(--el-text-color-regular); font-size: 13px; }
.payee-title { color: var(--el-text-color-primary); font-weight: 600; }
.payee-empty { color: var(--el-color-danger); }