新增、编辑、删除费用后,都会根据费用明细重新汇总订单状态:

- 全部未出账:UNBILLED
- 部分已出账、部分未出账:PARTIAL
- 全部已出账:BILLED
3. 应收账单生成
   - 预览、统计、分页生成支持 UNBILLED 和 PARTIAL;
   - PARTIAL 订单只读取未出账费用;
   - 生成成功后,只将本次实际生成的费用更新为 BILLED;
   - 不会重复带入历史已出账费用;
   - 账单金额只统计本次未出账费用。
4. 作废、取消、失败清理
   - 只恢复当前账单关联的费用;
   - 如果费用仍被其他有效账单使用,则继续保持 BILLED;
   - 根据费用状态重新汇总订单状态;
   - 大账单清理使用批量 SQL,不组装几十万条费用 ID。
This commit is contained in:
caojia
2026-09-22 17:20:27 +08:00
parent cb50d2a258
commit 221d4721ee
11 changed files with 457 additions and 106 deletions
@@ -0,0 +1,20 @@
package com.ruoyi.ar.enums;
/** 应收费用明细是否已经生成正式应收账单。 */
public enum ArIncomeBillFeeStatus
{
UNBILLED("UNBILLED"),
BILLED("BILLED");
private final String code;
ArIncomeBillFeeStatus(String code)
{
this.code = code;
}
public String getCode()
{
return code;
}
}
@@ -1,9 +1,10 @@
package com.ruoyi.ar.enums;
/** 计费订单的应收账单占用状态。 */
/** 计费订单的应收账单状态。 */
public enum ArOrderBillStatus
{
UNBILLED("UNBILLED"),
PARTIAL("PARTIAL"),
BILLED("BILLED");
private final String code;
@@ -13,6 +13,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
@@ -51,6 +52,7 @@ import com.ruoyi.ar.enums.ArBillGenerationStatus;
import com.ruoyi.ar.enums.ArBillSelectionMode;
import com.ruoyi.ar.enums.ArBillStatus;
import com.ruoyi.ar.enums.ArExportStatus;
import com.ruoyi.ar.enums.ArIncomeBillFeeStatus;
import com.ruoyi.ar.enums.ArLogObjectType;
import com.ruoyi.ar.enums.ArLogOperationSource;
import com.ruoyi.ar.enums.ArLogOperationType;
@@ -222,7 +224,7 @@ public class ArBillServiceImpl implements IArBillService {
return sourceBillMapper.selectPrcOrderBillByCustomerPeriod(query.getCustomerId(),
query.getCustomerCode(), StringUtils.trim(query.getOrderOrTrackingNo()),
StringUtils.trim(query.getExcludeOrderOrTrackingNo()), query.getBillPeriodEnd(), query.getCurrency(),
ArOrderBillStatus.UNBILLED.getCode(), null);
eligibleOrderBillStatuses(), ArIncomeBillFeeStatus.UNBILLED.getCode(), null);
}
/**
@@ -234,7 +236,7 @@ public class ArBillServiceImpl implements IArBillService {
return nvl(sourceBillMapper.sumPrcOrderBillIncomeByCustomerPeriod(query.getCustomerId(),
query.getCustomerCode(), StringUtils.trim(query.getOrderOrTrackingNo()),
StringUtils.trim(query.getExcludeOrderOrTrackingNo()), query.getBillPeriodEnd(), query.getCurrency(),
ArOrderBillStatus.UNBILLED.getCode()));
eligibleOrderBillStatuses(), ArIncomeBillFeeStatus.UNBILLED.getCode()));
}
/**
@@ -421,7 +423,8 @@ public class ArBillServiceImpl implements IArBillService {
log.info("开始统计应收账单生成范围,taskId={}", taskId);
int totalCount = sourceBillMapper.countPrcOrderBillByCustomerPeriod(
request.getCustomerId(), request.getCustomerCode(), StringUtils.trim(request.getOrderOrTrackingNo()),
request.getBillPeriodEnd(), request.getCurrency(), ArOrderBillStatus.UNBILLED.getCode(),
request.getBillPeriodEnd(), request.getCurrency(), eligibleOrderBillStatuses(),
ArIncomeBillFeeStatus.UNBILLED.getCode(),
task.getSnapshotCreateTime(), null, excludedSourceBillIds);
if (totalCount <= 0) {
throw new ServiceException("当前全选范围内没有可生成的订单");
@@ -545,7 +548,8 @@ public class ArBillServiceImpl implements IArBillService {
List<PrcOrderBill> sources = sourceBillMapper.selectPrcOrderBillByCustomerPeriodCursor(
request.getCustomerId(), request.getCustomerCode(), StringUtils.trim(request.getOrderOrTrackingNo()),
request.getBillPeriodEnd(), StringUtils.isEmpty(request.getCurrency()) ? "CNY" : request.getCurrency(),
ArOrderBillStatus.UNBILLED.getCode(), task.getSnapshotCreateTime(), task.getLastSourceBillId(),
eligibleOrderBillStatuses(), ArIncomeBillFeeStatus.UNBILLED.getCode(),
task.getSnapshotCreateTime(), task.getLastSourceBillId(),
explicitIds, selectionMode == ArBillSelectionMode.ALL_MATCHED ? excludedSourceBillIds : null,
GENERATE_PAGE_SIZE);
if (sources == null || sources.isEmpty()) {
@@ -563,13 +567,36 @@ public class ArBillServiceImpl implements IArBillService {
throw new ServiceException("截止日期内存在多个币种,不能合并为一张账单");
}
}
// 先抓取本批次的未出账费用快照,后续只更新和复制这些费用,避免把同订单已有 BILLED 费用重复带入新账单。
List<PrcIncomeBillFee> sourceFees = sourceFeeMapper.selectUnbilledByBillIds(
currentSourceBillIds, ArIncomeBillFeeStatus.UNBILLED.getCode());
if (sourceFees == null || sourceFees.isEmpty()) {
throw new ServiceException("当前批次没有可生成的未出账费用");
}
Map<String, Integer> sourceFeeCounts = new HashMap<>();
List<String> sourceFeeIds = new ArrayList<>(sourceFees.size());
for (PrcIncomeBillFee sourceFee : sourceFees) {
sourceFeeCounts.merge(sourceFee.getBillId(), 1, Integer::sum);
sourceFeeIds.add(sourceFee.getFeeId());
}
for (String sourceBillId : currentSourceBillIds) {
if (!sourceFeeCounts.containsKey(sourceBillId)) {
throw new ServiceException("来源订单没有可生成的未出账费用:" + sourceBillId);
}
}
int claimedCount = sourceBillMapper.claimArBillOrders(currentSourceBillIds, task.getCreateBy(),
ArOrderBillStatus.UNBILLED.getCode(), ArOrderBillStatus.BILLED.getCode());
eligibleOrderBillStatuses(), ArOrderBillStatus.BILLED.getCode());
if (claimedCount != currentSourceBillIds.size()) {
throw new ServiceException("部分计费订单已被其他账单占用,请重新查询后生成");
}
sourceFeeMapper.updateArBillStatusByBillIds(currentSourceBillIds,
ArOrderBillStatus.UNBILLED.getCode(), ArOrderBillStatus.BILLED.getCode());
int billedFeeCount = sourceFeeMapper.updateArBillStatusByFeeIds(sourceFeeIds,
ArIncomeBillFeeStatus.UNBILLED.getCode(), ArIncomeBillFeeStatus.BILLED.getCode());
if (billedFeeCount != sourceFeeIds.size()) {
throw new ServiceException("部分费用状态已发生变化,请重新查询后生成");
}
sourceBillMapper.refreshArBillStatusByBillIds(currentSourceBillIds);
int detailNo = (task.getProcessedCount() == null ? 0 : task.getProcessedCount()) + 1;
List<ArBillDetail> details = new ArrayList<>(sources.size());
@@ -586,7 +613,6 @@ public class ArBillServiceImpl implements IArBillService {
throw new ServiceException("账单明细写入数量不一致");
}
List<PrcIncomeBillFee> sourceFees = sourceFeeMapper.selectByBillIds(currentSourceBillIds);
List<ArBillDetailFee> detailFees = new ArrayList<>(sourceFees == null ? 0 : sourceFees.size());
Map<String, Integer> feeNumbers = new HashMap<>();
if (sourceFees != null) {
@@ -625,18 +651,9 @@ public class ArBillServiceImpl implements IArBillService {
*/
private void cleanupGenerateTask(ArBillGenerateTask task, String message, boolean canceled,
String operator, String requestId) {
List<String> sourceBillIds = new ArrayList<>();
for (ArBillDetail detail : detailMapper.selectByBillId(task.getArBillId())) {
if (!StringUtils.isEmpty(detail.getPrcOrderBillId())) {
sourceBillIds.add(detail.getPrcOrderBillId());
}
}
sourceBillMapper.releaseArBillOrdersByArBillId(task.getArBillId(), operator,
ArOrderBillStatus.BILLED.getCode(), ArOrderBillStatus.UNBILLED.getCode());
if (!sourceBillIds.isEmpty()) {
sourceFeeMapper.updateArBillStatusByBillIds(sourceBillIds,
ArOrderBillStatus.BILLED.getCode(), ArOrderBillStatus.UNBILLED.getCode());
}
// 当前任务账单仍处于草稿状态,恢复时排除当前账单自身的关联,避免组装超大的来源 ID 集合。
sourceFeeMapper.restoreUnbilledIfNoActiveArBillByArBillId(task.getArBillId());
sourceBillMapper.refreshArBillStatusByArBillId(task.getArBillId());
detailFeeMapper.deleteByBillId(task.getArBillId());
detailMapper.deleteByBillId(task.getArBillId());
billMapper.updateArBillGenerationStatus(task.getArBillId(), ArBillGenerationStatus.FAILED, operator, message);
@@ -781,28 +798,14 @@ public class ArBillServiceImpl implements IArBillService {
bill.getStatus(),
"当前状态不允许作废", operator, requestId);
}
List<String> sourceBillIds = new ArrayList<>();
for (ArBillDetail detail : detailMapper.selectByBillId(arBillId)) {
if (!StringUtils.isEmpty(detail.getPrcOrderBillId())) {
sourceBillIds.add(detail.getPrcOrderBillId());
}
}
if (billMapper.updateArBillStatus(arBillId, ArBillStatus.VOID.getCode(), remark, operator, currentStatus.getCode()) != 1) {
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID,
bill.getStatus(),
"账单状态已发生变化,请刷新后重试", operator, requestId);
}
if (!sourceBillIds.isEmpty()) {
int releasedCount = sourceBillMapper.releaseArBillOrders(sourceBillIds, operator,
ArOrderBillStatus.BILLED.getCode(), ArOrderBillStatus.UNBILLED.getCode());
if (releasedCount != sourceBillIds.size()) {
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID,
bill.getStatus(),
"部分来源订单状态异常,账单未作废,请刷新后重试", operator, requestId);
}
sourceFeeMapper.updateArBillStatusByBillIds(sourceBillIds,
ArOrderBillStatus.BILLED.getCode(), ArOrderBillStatus.UNBILLED.getCode());
}
// 作废后保留历史明细,只释放本账单关联的费用;若费用仍被其他有效账单引用,则继续保持 BILLED。
sourceFeeMapper.restoreUnbilledIfNoActiveArBillByArBillId(arBillId);
sourceBillMapper.refreshArBillStatusByArBillId(arBillId);
recordSuccess(bill, ArLogObjectType.BILL, ArLogOperationType.VOID,
"作废应收账单:" + remark, bill.getStatus(), ArBillStatus.VOID.getCode(), requestId, operator);
return selectArBillById(arBillId);
@@ -1667,6 +1670,11 @@ public class ArBillServiceImpl implements IArBillService {
}
}
/** 可参与本次应收生成的订单状态;PARTIAL 只生成其中仍为 UNBILLED 的费用。 */
private List<String> eligibleOrderBillStatuses() {
return Arrays.asList(ArOrderBillStatus.UNBILLED.getCode(), ArOrderBillStatus.PARTIAL.getCode());
}
private Date earliestBizDate(List<PrcOrderBill> sources) {
Date earliest = null;
for (PrcOrderBill source : sources) {
@@ -23,7 +23,7 @@ public class PrcIncomeBillFee implements Serializable
private String currency;
private String roundMode;
private String remark;
/** 是否生成账单状态:UNBILLED/BILLED */
/** 是否生成账单状态:UNBILLED未生成/BILLED已生成 */
private String arBillStatus;
/** 是否手动增加:0否 1是 */
private String manualFlag;
@@ -97,7 +97,7 @@ public class PrcOrderBill extends BaseEntity
/** payable total */
private BigDecimal payTotal;
private String currency;
/** 应收账单占用状态:UNBILLED/BILLED。 */
/** 应收账单状态:UNBILLED未出账/PARTIAL部分出账/BILLED已出账。 */
private String arBillStatus;
/** ACTIVE / MISSING / DISABLED */
private String customerActiveStatus;
@@ -11,6 +11,10 @@ public interface PrcIncomeBillFeeMapper
public List<PrcIncomeBillFee> selectByBillIds(@Param("billIds") List<String> billIds);
/** 查询指定计费订单中尚未生成应收账单的费用。 */
public List<PrcIncomeBillFee> selectUnbilledByBillIds(@Param("billIds") List<String> billIds,
@Param("arBillStatus") String arBillStatus);
public PrcIncomeBillFee selectByFeeId(String feeId);
/** Count fees of a bill under given ar_bill_status and fee_type (e.g. BILLED + BASIC_FREIGHT). */
@@ -26,6 +30,16 @@ public interface PrcIncomeBillFeeMapper
public int updateArBillStatusByBillIds(@Param("billIds") List<String> billIds,
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
/** 按费用明细 ID 精确更新状态,避免误更新同一订单的历史费用。 */
public int updateArBillStatusByFeeIds(@Param("feeIds") List<String> feeIds,
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
/** 作废或失败清理后,仅在不存在其他有效账单关联时恢复为未出账。 */
public int restoreUnbilledIfNoActiveArBill(@Param("feeIds") List<String> feeIds);
/** 按应收账单批量恢复其关联费用,避免大账单组装超大的费用 ID 列表。 */
public int restoreUnbilledIfNoActiveArBillByArBillId(@Param("arBillId") Long arBillId);
public int deleteByFeeId(String feeId);
public int deleteByBillId(String billId);
@@ -29,32 +29,35 @@ public interface PrcOrderBillMapper
/** 转单号下拉搜索:按关键词模糊匹配,返回去重后的转单号(按最近创建排序,限量返回) */
public List<String> selectTransferNoOptions(@Param("keyword") String keyword, @Param("limit") int limit);
/** 应收账单生成读取指定客户截止日期前的未出账订单计费快照。 */
/** 应收账单生成读取指定客户截止日期前仍存在未出账费用的订单计费快照。 */
public List<PrcOrderBill> selectPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
@Param("excludeOrderOrTrackingNo") String excludeOrderOrTrackingNo,
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
@Param("arBillStatus") String arBillStatus, @Param("excludedBillIds") List<String> excludedBillIds);
@Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus,
@Param("excludedBillIds") List<String> excludedBillIds);
/** 统计应收账单预览结果金额,供全选模式展示。 */
BigDecimal sumPrcOrderBillIncomeByCustomerPeriod(@Param("customerId") Long customerId,
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
@Param("excludeOrderOrTrackingNo") String excludeOrderOrTrackingNo,
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
@Param("arBillStatus") String arBillStatus);
@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("cutoffDate") Date cutoffDate, @Param("currency") String currency,
@Param("arBillStatus") String arBillStatus, @Param("snapshotCreateTime") Date snapshotCreateTime,
@Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus,
@Param("snapshotCreateTime") Date snapshotCreateTime,
@Param("lastSourceBillId") String lastSourceBillId, @Param("billIds") List<String> billIds,
@Param("excludedBillIds") List<String> excludedBillIds, @Param("limit") int limit);
int countPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
@Param("arBillStatus") String arBillStatus, @Param("snapshotCreateTime") Date snapshotCreateTime,
@Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus,
@Param("snapshotCreateTime") Date snapshotCreateTime,
@Param("billIds") List<String> billIds, @Param("excludedBillIds") List<String> excludedBillIds);
/** 按生成账单时用户勾选的订单读取,并再次校验账单筛选条件。 */
@@ -62,11 +65,11 @@ public interface PrcOrderBillMapper
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
@Param("cutoffDate") Date cutoffDate,
@Param("currency") String currency, @Param("billIds") List<String> billIds,
@Param("arBillStatus") String arBillStatus);
@Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus);
/** 原子占用订单,防止同一订单被并发生成到多张应收账单。 */
public int claimArBillOrders(@Param("billIds") List<String> billIds, @Param("operator") String operator,
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
@Param("arBillStatuses") List<String> arBillStatuses, @Param("toStatus") String toStatus);
/** 作废账单后释放订单,允许后续重新生成新版本账单。 */
public int releaseArBillOrders(@Param("billIds") List<String> billIds, @Param("operator") String operator,
@@ -75,6 +78,12 @@ public interface PrcOrderBillMapper
int releaseArBillOrdersByArBillId(@Param("arBillId") Long arBillId, @Param("operator") String operator,
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
/** 根据费用明细状态汇总订单的 UNBILLED/PARTIAL/BILLED 状态。 */
int refreshArBillStatusByBillIds(@Param("billIds") List<String> billIds);
/** 按应收账单关联的来源订单批量汇总状态,适用于大账单清理和作废。 */
int refreshArBillStatusByArBillId(@Param("arBillId") Long arBillId);
public int insertPrcOrderBill(PrcOrderBill bill);
public int updatePrcOrderBill(PrcOrderBill bill);
@@ -16,6 +16,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.ar.enums.ArIncomeBillFeeStatus;
import com.ruoyi.ar.enums.ArOrderBillStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
@@ -253,7 +254,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
fee.setCurrency(currency);
fee.setRoundMode(StringUtils.isEmpty(surcharge.getRoundMode()) ? bill.getRoundMode() : surcharge.getRoundMode());
fee.setRemark(request.getRemark());
fee.setArBillStatus(ArOrderBillStatus.UNBILLED.getCode());
fee.setArBillStatus(ArIncomeBillFeeStatus.UNBILLED.getCode());
fee.setManualFlag("1");
incomeBillFeeMapper.insertPrcIncomeBillFee(fee);
@@ -349,7 +350,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{
throw new ServiceException(MessageUtils.message("price.bill.fee.notManual"));
}
if (ArOrderBillStatus.BILLED.getCode().equals(fee.getArBillStatus()))
if (ArIncomeBillFeeStatus.BILLED.getCode().equals(fee.getArBillStatus()))
{
throw new ServiceException(MessageUtils.message("price.bill.fee.alreadyBilled"));
}
@@ -400,6 +401,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
? BillCustomerActiveStatus.ACTIVE.getCode() : bill.getCustomerActiveStatus());
bill.setCreditArrearsStatus(creditArrearsStatus);
orderBillMapper.updatePrcOrderBill(bill);
orderBillMapper.refreshArBillStatusByBillIds(Arrays.asList(bill.getBillId()));
return toResult(bill, fees, payBillFeeMapper.selectByBillId(bill.getBillId()));
}
@@ -639,6 +641,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{
incomeBillFeeMapper.insertBatch(fees);
}
orderBillMapper.refreshArBillStatusByBillIds(Arrays.asList(bill.getBillId()));
PrcPayBill payBill = persistPayBill(bill);
List<PrcPayBillFee> payFees = toPayFeeRows(bill.getBillId(),
payBill == null ? null : payBill.getPayBillId(), result);
@@ -810,7 +813,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
return;
}
int billedFreightCount = incomeBillFeeMapper.countByBillIdAndArBillStatusAndFeeType(
existed.getBillId(), ArOrderBillStatus.BILLED.getCode(), "BASIC_FREIGHT");
existed.getBillId(), ArIncomeBillFeeStatus.BILLED.getCode(), "BASIC_FREIGHT");
if (billedFreightCount > 0)
{
throw new ServiceException(MessageUtils.message("price.bill.alreadyInArBill", existed.getOrderNo()));
@@ -842,7 +845,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
fee.setCurrency(line.getCurrency());
fee.setRoundMode(line.getRoundMode());
fee.setRemark(line.getRemark());
fee.setArBillStatus(ArOrderBillStatus.UNBILLED.getCode());
fee.setArBillStatus(ArIncomeBillFeeStatus.UNBILLED.getCode());
fee.setManualFlag("0");
fees.add(fee);
}
@@ -39,6 +39,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
order by bill_id, field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE'), fee_id
</select>
<select id="selectUnbilledByBillIds" resultMap="PrcIncomeBillFeeResult">
select fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
ar_bill_status, manual_flag, create_time
from prc_income_bill_fee
where ar_bill_status = #{arBillStatus}
and bill_id in
<foreach collection="billIds" item="billId" open="(" separator="," close=")">
#{billId}
</foreach>
order by bill_id, field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE'), fee_id
</select>
<select id="selectByFeeId" parameterType="String" resultMap="PrcIncomeBillFeeResult">
select fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
ar_bill_status, manual_flag, create_time
@@ -98,6 +110,52 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach>
</update>
<update id="updateArBillStatusByFeeIds">
update prc_income_bill_fee
set ar_bill_status = #{toStatus}
where ar_bill_status = #{fromStatus}
and fee_id in
<foreach collection="feeIds" item="feeId" open="(" separator="," close=")">
#{feeId}
</foreach>
</update>
<update id="restoreUnbilledIfNoActiveArBill">
update prc_income_bill_fee f
set f.ar_bill_status = 'UNBILLED'
where f.ar_bill_status = 'BILLED'
and f.fee_id in
<foreach collection="feeIds" item="feeId" open="(" separator="," close=")">
#{feeId}
</foreach>
and not exists (
select 1
from ar_bill_detail_fee df
inner join ar_bill_detail d on d.detail_id = df.detail_id
inner join ar_bill b on b.ar_bill_id = d.ar_bill_id
where df.prc_income_bill_fee_id = f.fee_id
and b.status &lt;&gt; 'VOID'
)
</update>
<update id="restoreUnbilledIfNoActiveArBillByArBillId">
update prc_income_bill_fee f
inner join ar_bill_detail_fee current_df on current_df.prc_income_bill_fee_id = f.fee_id
inner join ar_bill_detail current_d on current_d.detail_id = current_df.detail_id
and current_d.ar_bill_id = #{arBillId}
set f.ar_bill_status = 'UNBILLED'
where f.ar_bill_status = 'BILLED'
and not exists (
select 1
from ar_bill_detail_fee other_df
inner join ar_bill_detail other_d on other_d.detail_id = other_df.detail_id
inner join ar_bill other_b on other_b.ar_bill_id = other_d.ar_bill_id
where other_df.prc_income_bill_fee_id = f.fee_id
and other_b.status &lt;&gt; 'VOID'
and other_b.ar_bill_id &lt;&gt; #{arBillId}
)
</update>
<delete id="deleteByFeeId" parameterType="String">
delete from prc_income_bill_fee where fee_id = #{feeId}
</delete>
@@ -92,6 +92,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
from prc_order_bill
</sql>
<!-- 应收生成专用来源快照:income_total 只汇总当前仍未出账的费用。 -->
<sql id="selectArBillSourceVo">
select p.bill_id, p.bill_key, p.order_no, p.tracking_no, p.transfer_no,
p.belong_saler, p.belong_saler_code, p.settlement_man, p.settlement_man_code, p.service_man,
p.customer_id, p.customer_code, p.customer_name,
p.pieces, p.actual_weight, p.volume, p.volume_weight, p.pkg_length, p.pkg_width, p.pkg_height,
p.shipper_name, p.consignee_name, p.dest_postcode, p.warehouse_code, p.order_time, p.order_remark,
p.product_code, p.product_name, p.country_code, p.country_name, p.weight, p.biz_date, p.tms_id,
p.match_source, p.match_message, p.price_set_id, p.price_set_code, p.price_set_name,
p.set_type, p.bind_priority, p.customer_priceset_id, p.psv_id, p.psd_id, p.product_country_id,
p.weight_min, p.weight_max, p.weight_label, p.effective_date, p.charge_weight,
p.freight_rate, p.base_freight_rate, p.discount_percent, p.registration_fee,
p.freight_amount, p.surcharge_amount,
(select coalesce(sum(f.amount), 0)
from prc_income_bill_fee f
where f.bill_id = p.bill_id
and f.ar_bill_status = #{feeArBillStatus}) as income_total,
p.pay_total, p.currency, p.ar_bill_status,
p.customer_active_status, p.credit_arrears_status, p.round_mode,
p.created_by, p.create_time, p.updated_by, p.update_time
from prc_order_bill p
</sql>
<select id="selectPrcOrderBillByOrderNo" parameterType="String" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
where order_no = #{orderNo}
@@ -245,85 +268,106 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<!-- rename_bill_tables.sql 已将需求文档中的 prc_bill 映射为本项目的 prc_order_bill。 -->
<!-- 生成弹窗预览:按客户、截止日、币种和订单号/运单号/转单号 OR 条件分页查询未入账订单。 -->
<select id="selectPrcOrderBillByCustomerPeriod" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
where customer_id = #{customerId}
<include refid="selectArBillSourceVo"/>
where p.customer_id = #{customerId}
<if test="customerCode != null and customerCode != ''">
and customer_code like concat('%', #{customerCode}, '%')
and p.customer_code like concat('%', #{customerCode}, '%')
</if>
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
and (order_no like concat('%', #{orderOrTrackingNo}, '%')
or tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
and (p.order_no like concat('%', #{orderOrTrackingNo}, '%')
or p.tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or p.transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
</if>
<if test="excludeOrderOrTrackingNo != null and excludeOrderOrTrackingNo != ''">
and (order_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or tracking_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or transfer_no like concat('%', #{excludeOrderOrTrackingNo}, '%'))
and (p.order_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or p.tracking_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or p.transfer_no like concat('%', #{excludeOrderOrTrackingNo}, '%'))
</if>
and biz_date &lt;= #{cutoffDate}
and ar_bill_status = #{arBillStatus}
and p.biz_date &lt;= #{cutoffDate}
and p.ar_bill_status in
<foreach collection="arBillStatuses" item="status" open="(" separator="," close=")">
#{status}
</foreach>
and exists (
select 1 from prc_income_bill_fee f
where f.bill_id = p.bill_id
and f.ar_bill_status = #{feeArBillStatus}
)
<if test="currency != null and currency != ''">
and currency = #{currency}
and p.currency = #{currency}
</if>
<if test="excludedBillIds != null and excludedBillIds.size() > 0">
and bill_id not in
and p.bill_id not in
<foreach collection="excludedBillIds" item="excludedBillId" open="(" separator="," close=")">
#{excludedBillId}
</foreach>
</if>
order by biz_date, order_no, bill_id
order by p.biz_date, p.order_no, p.bill_id
</select>
<select id="sumPrcOrderBillIncomeByCustomerPeriod" resultType="java.math.BigDecimal">
select coalesce(sum(income_total), 0)
from prc_order_bill
where customer_id = #{customerId}
select coalesce(sum(f.amount), 0)
from prc_order_bill p
inner join prc_income_bill_fee f on f.bill_id = p.bill_id
and f.ar_bill_status = #{feeArBillStatus}
where p.customer_id = #{customerId}
<if test="customerCode != null and customerCode != ''">
and customer_code like concat('%', #{customerCode}, '%')
and p.customer_code like concat('%', #{customerCode}, '%')
</if>
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
and (order_no like concat('%', #{orderOrTrackingNo}, '%')
or tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
and (p.order_no like concat('%', #{orderOrTrackingNo}, '%')
or p.tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or p.transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
</if>
<if test="excludeOrderOrTrackingNo != null and excludeOrderOrTrackingNo != ''">
and (order_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or tracking_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or transfer_no like concat('%', #{excludeOrderOrTrackingNo}, '%'))
and (p.order_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or p.tracking_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
or p.transfer_no like concat('%', #{excludeOrderOrTrackingNo}, '%'))
</if>
and biz_date &lt;= #{cutoffDate}
and ar_bill_status = #{arBillStatus}
and p.biz_date &lt;= #{cutoffDate}
and p.ar_bill_status in
<foreach collection="arBillStatuses" item="status" open="(" separator="," close=")">
#{status}
</foreach>
<if test="currency != null and currency != ''">
and currency = #{currency}
and p.currency = #{currency}
</if>
</select>
<sql id="customerPeriodConditions">
where customer_id = #{customerId}
where p.customer_id = #{customerId}
<if test="customerCode != null and customerCode != ''">
and customer_code like concat('%', #{customerCode}, '%')
and p.customer_code like concat('%', #{customerCode}, '%')
</if>
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
and (order_no like concat('%', #{orderOrTrackingNo}, '%')
or tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
and (p.order_no like concat('%', #{orderOrTrackingNo}, '%')
or p.tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or p.transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
</if>
and biz_date &lt;= #{cutoffDate}
and ar_bill_status = #{arBillStatus}
and p.biz_date &lt;= #{cutoffDate}
and p.ar_bill_status in
<foreach collection="arBillStatuses" item="status" open="(" separator="," close=")">
#{status}
</foreach>
and exists (
select 1 from prc_income_bill_fee f
where f.bill_id = p.bill_id
and f.ar_bill_status = #{feeArBillStatus}
)
<if test="currency != null and currency != ''">
and currency = #{currency}
and p.currency = #{currency}
</if>
<if test="snapshotCreateTime != null">
and (create_time is null or create_time &lt;= #{snapshotCreateTime})
and (p.create_time is null or p.create_time &lt;= #{snapshotCreateTime})
</if>
<if test="billIds != null and billIds.size() > 0">
and bill_id in
and p.bill_id in
<foreach collection="billIds" item="billId" open="(" separator="," close=")">
#{billId}
</foreach>
</if>
<if test="excludedBillIds != null and excludedBillIds.size() > 0">
and bill_id not in
and p.bill_id not in
<foreach collection="excludedBillIds" item="excludedBillId" open="(" separator="," close=")">
#{excludedBillId}
</foreach>
@@ -332,43 +376,51 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<!-- 异步生成使用 bill_id 游标批量读取,避免深分页和一次加载全量订单。 -->
<select id="selectPrcOrderBillByCustomerPeriodCursor" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
<include refid="selectArBillSourceVo"/>
<include refid="customerPeriodConditions"/>
<if test="lastSourceBillId != null and lastSourceBillId != ''">
and bill_id &gt; #{lastSourceBillId}
and p.bill_id &gt; #{lastSourceBillId}
</if>
order by bill_id
order by p.bill_id
limit #{limit}
</select>
<!-- 全选模式先统计总数,用于任务进度和空范围校验。 -->
<select id="countPrcOrderBillByCustomerPeriod" resultType="int">
select count(1)
from prc_order_bill
from prc_order_bill p
<include refid="customerPeriodConditions"/>
</select>
<select id="selectPrcOrderBillByCustomerCutoffAndIds" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
where customer_id = #{customerId}
and biz_date &lt;= #{cutoffDate}
and ar_bill_status = #{arBillStatus}
and bill_id in
<include refid="selectArBillSourceVo"/>
where p.customer_id = #{customerId}
and p.biz_date &lt;= #{cutoffDate}
and p.ar_bill_status in
<foreach collection="arBillStatuses" item="status" open="(" separator="," close=")">
#{status}
</foreach>
and exists (
select 1 from prc_income_bill_fee f
where f.bill_id = p.bill_id
and f.ar_bill_status = #{feeArBillStatus}
)
and p.bill_id in
<foreach collection="billIds" item="billId" open="(" separator="," close=")">
#{billId}
</foreach>
<if test="customerCode != null and customerCode != ''">
and customer_code like concat('%', #{customerCode}, '%')
and p.customer_code like concat('%', #{customerCode}, '%')
</if>
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
and (order_no like concat('%', #{orderOrTrackingNo}, '%')
or tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
and (p.order_no like concat('%', #{orderOrTrackingNo}, '%')
or p.tracking_no like concat('%', #{orderOrTrackingNo}, '%')
or p.transfer_no like concat('%', #{orderOrTrackingNo}, '%'))
</if>
<if test="currency != null and currency != ''">
and currency = #{currency}
and p.currency = #{currency}
</if>
order by biz_date, order_no, bill_id
order by p.biz_date, p.order_no, p.bill_id
</select>
<insert id="insertPrcOrderBill" parameterType="PrcOrderBill">
@@ -479,11 +531,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where bill_id = #{billId}
</update>
<!-- 抢占来源订单:只有未入账订单可以变更为已入账,防止重复生成。 -->
<!-- 抢占来源订单:未出账或部分出账订单均可进入本次生成事务。 -->
<update id="claimArBillOrders">
update prc_order_bill
set ar_bill_status = #{toStatus}, updated_by = #{operator}, update_time = sysdate()
where ar_bill_status = #{fromStatus}
where ar_bill_status in
<foreach collection="arBillStatuses" item="status" open="(" separator="," close=")">
#{status}
</foreach>
and bill_id in
<foreach collection="billIds" item="billId" open="(" separator="," close=")">
#{billId}
@@ -507,4 +562,63 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where d.ar_bill_id = #{arBillId}
and p.ar_bill_status = #{fromStatus}
</update>
<!-- 订单状态由来源费用状态汇总,避免补录或删除费用后状态失真。 -->
<update id="refreshArBillStatusByBillIds">
update prc_order_bill o
left join (
select bill_id,
count(1) as fee_count,
sum(case when ar_bill_status = 'BILLED' then 1 else 0 end) as billed_count,
sum(case when ar_bill_status = 'UNBILLED' then 1 else 0 end) as unbilled_count
from prc_income_bill_fee
where bill_id in
<foreach collection="billIds" item="billId" open="(" separator="," close=")">
#{billId}
</foreach>
group by bill_id
) f on f.bill_id = o.bill_id
set o.ar_bill_status = case
when f.bill_id is null then 'UNBILLED'
when f.unbilled_count > 0 and f.billed_count > 0 then 'PARTIAL'
when f.unbilled_count > 0 then 'UNBILLED'
when f.billed_count = f.fee_count then 'BILLED'
else 'UNBILLED'
end,
o.update_time = sysdate()
where o.bill_id in
<foreach collection="billIds" item="billId" open="(" separator="," close=")">
#{billId}
</foreach>
</update>
<update id="refreshArBillStatusByArBillId">
update prc_order_bill o
inner join (
select distinct d.prc_order_bill_id
from ar_bill_detail d
where d.ar_bill_id = #{arBillId}
) source_order on source_order.prc_order_bill_id = o.bill_id
left join (
select f.bill_id,
count(1) as fee_count,
sum(case when f.ar_bill_status = 'BILLED' then 1 else 0 end) as billed_count,
sum(case when f.ar_bill_status = 'UNBILLED' then 1 else 0 end) as unbilled_count
from prc_income_bill_fee f
inner join (
select distinct d2.prc_order_bill_id
from ar_bill_detail d2
where d2.ar_bill_id = #{arBillId}
) source_order2 on source_order2.prc_order_bill_id = f.bill_id
group by f.bill_id
) fee_summary on fee_summary.bill_id = o.bill_id
set o.ar_bill_status = case
when fee_summary.bill_id is null then 'UNBILLED'
when fee_summary.unbilled_count > 0 and fee_summary.billed_count > 0 then 'PARTIAL'
when fee_summary.unbilled_count > 0 then 'UNBILLED'
when fee_summary.billed_count = fee_summary.fee_count then 'BILLED'
else 'UNBILLED'
end,
o.update_time = sysdate()
</update>
</mapper>
@@ -0,0 +1,124 @@
-- 应收账单来源费用状态迁移(统一脚本)
--
-- 目标:
-- 1. prc_order_bill.ar_bill_status:UNBILLED 未出账、PARTIAL 部分出账、BILLED 已出账;
-- 2. prc_income_bill_fee.ar_bill_status:UNBILLED 未生成、BILLED 已生成;
-- 3. 按现有有效应收账单关联回填费用状态,再由费用状态汇总回填订单状态。
--
-- 脚本可重复执行。执行前请备份数据库,并确认 ar_bill_detail_fee.prc_income_bill_fee_id
-- 已经正确保存来源费用 ID。
SET NAMES utf8mb4;
SET @db_name := DATABASE();
-- 1. 来源计费订单状态字段及索引
SET @has_order_status := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
AND TABLE_NAME = 'prc_order_bill'
AND COLUMN_NAME = 'ar_bill_status'
);
SET @sql := IF(@has_order_status = 0,
'ALTER TABLE prc_order_bill ADD COLUMN ar_bill_status VARCHAR(20) NOT NULL DEFAULT ''UNBILLED'' COMMENT ''应收账单状态:UNBILLED未出账/PARTIAL部分出账/BILLED已出账'' AFTER currency',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
ALTER TABLE prc_order_bill
MODIFY COLUMN ar_bill_status VARCHAR(20) NOT NULL DEFAULT 'UNBILLED'
COMMENT '应收账单状态:UNBILLED未出账/PARTIAL部分出账/BILLED已出账';
SET @has_order_status_index := (
SELECT COUNT(*)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name
AND TABLE_NAME = 'prc_order_bill'
AND INDEX_NAME = 'idx_bill_ar_status'
);
SET @sql := IF(@has_order_status_index = 0,
'ALTER TABLE prc_order_bill ADD KEY idx_bill_ar_status (customer_id, currency, ar_bill_status, biz_date)',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 2. 来源费用状态字段及索引(兼容同事已提前加列的数据库)
SET @has_fee_status := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
AND TABLE_NAME = 'prc_income_bill_fee'
AND COLUMN_NAME = 'ar_bill_status'
);
SET @sql := IF(@has_fee_status = 0,
'ALTER TABLE prc_income_bill_fee ADD COLUMN ar_bill_status VARCHAR(20) NOT NULL DEFAULT ''UNBILLED'' COMMENT ''是否生成账单状态:UNBILLED未生成/BILLED已生成'' AFTER remark',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
ALTER TABLE prc_income_bill_fee
MODIFY COLUMN ar_bill_status VARCHAR(20) NOT NULL DEFAULT 'UNBILLED'
COMMENT '是否生成账单状态:UNBILLED未生成/BILLED已生成';
SET @has_fee_status_index := (
SELECT COUNT(*)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name
AND TABLE_NAME = 'prc_income_bill_fee'
AND INDEX_NAME = 'idx_income_fee_ar_status'
);
SET @sql := IF(@has_fee_status_index = 0,
'ALTER TABLE prc_income_bill_fee ADD KEY idx_income_fee_ar_status (ar_bill_status)',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @has_fee_bill_status_index := (
SELECT COUNT(*)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name
AND TABLE_NAME = 'prc_income_bill_fee'
AND INDEX_NAME = 'idx_income_fee_bill_ar_status'
);
SET @sql := IF(@has_fee_bill_status_index = 0,
'ALTER TABLE prc_income_bill_fee ADD KEY idx_income_fee_bill_ar_status (bill_id, ar_bill_status)',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 3. 根据有效应收账单关联回填费用状态。
-- 作废账单不再占用来源费用;同一费用只要仍关联一张有效账单,就保持 BILLED。
UPDATE prc_income_bill_fee f
LEFT JOIN (
SELECT DISTINCT df.prc_income_bill_fee_id AS fee_id
FROM ar_bill_detail_fee df
INNER JOIN ar_bill_detail d ON d.detail_id = df.detail_id
INNER JOIN ar_bill b ON b.ar_bill_id = d.ar_bill_id
WHERE b.status <> 'VOID'
) active_fee ON active_fee.fee_id = f.fee_id
SET f.ar_bill_status = CASE
WHEN active_fee.fee_id IS NOT NULL THEN 'BILLED'
ELSE 'UNBILLED'
END;
-- 4. 根据费用状态回填订单状态。
UPDATE prc_order_bill o
LEFT JOIN (
SELECT bill_id,
COUNT(*) AS fee_count,
SUM(CASE WHEN ar_bill_status = 'BILLED' THEN 1 ELSE 0 END) AS billed_count,
SUM(CASE WHEN ar_bill_status = 'UNBILLED' THEN 1 ELSE 0 END) AS unbilled_count
FROM prc_income_bill_fee
GROUP BY bill_id
) fee_summary ON fee_summary.bill_id = o.bill_id
SET o.ar_bill_status = CASE
WHEN fee_summary.bill_id IS NULL THEN 'UNBILLED'
WHEN fee_summary.unbilled_count > 0 AND fee_summary.billed_count > 0 THEN 'PARTIAL'
WHEN fee_summary.unbilled_count > 0 THEN 'UNBILLED'
WHEN fee_summary.billed_count = fee_summary.fee_count THEN 'BILLED'
ELSE 'UNBILLED'
END;