✨ 应收账单保存计费失败状态并导出说明及黄色标注

This commit is contained in:
caojia
2026-10-14 17:50:03 +08:00
parent 996d854492
commit e4438d6f01
8 changed files with 157 additions and 51 deletions
@@ -28,6 +28,8 @@ public class ArBillDetail implements Serializable {
private String productCode; private String productCode;
private String productName; private String productName;
private String goodsDescription; private String goodsDescription;
/** 生成账单时的计费失败状态快照:0成功/1未匹配产品/2未匹配国家/3未匹配重量段。 */
private Integer billingFailStatus;
private String countryCode; private String countryCode;
private String countryName; private String countryName;
private String destPostcode; private String destPostcode;
@@ -28,6 +28,10 @@ import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.xssf.model.StylesTable;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
@@ -85,6 +89,7 @@ import com.bms.common.utils.uuid.IdUtils;
import com.bms.price.domain.PrcIncomeBill; import com.bms.price.domain.PrcIncomeBill;
import com.bms.price.domain.PrcIncomeBillFee; import com.bms.price.domain.PrcIncomeBillFee;
import com.bms.price.domain.PrcOrderBill; import com.bms.price.domain.PrcOrderBill;
import com.bms.price.enums.BillBillingFailStatus;
import com.bms.price.mapper.PrcIncomeBillMapper; import com.bms.price.mapper.PrcIncomeBillMapper;
import com.bms.price.mapper.PrcIncomeBillFeeMapper; import com.bms.price.mapper.PrcIncomeBillFeeMapper;
import com.bms.price.mapper.PrcOrderBillMapper; import com.bms.price.mapper.PrcOrderBillMapper;
@@ -105,7 +110,7 @@ public class ArBillServiceImpl implements IArBillService {
*/ */
private static final int TEMPLATE_DATA_START_ROW = 5; private static final int TEMPLATE_DATA_START_ROW = 5;
private static final int TEMPLATE_FOOTER_START_ROW = 5; private static final int TEMPLATE_FOOTER_START_ROW = 5;
private static final int TEMPLATE_COLUMN_COUNT = 20; private static final int TEMPLATE_COLUMN_COUNT = 21;
/** 客户订单号、运单号位于 Excel 的 C、D 列,按给定样例长度并额外保留两字符边距。 */ /** 客户订单号、运单号位于 Excel 的 C、D 列,按给定样例长度并额外保留两字符边距。 */
private static final int CUSTOMER_ORDER_NO_COLUMN_INDEX = 3; private static final int CUSTOMER_ORDER_NO_COLUMN_INDEX = 3;
private static final int TRACKING_NO_COLUMN_INDEX = 4; private static final int TRACKING_NO_COLUMN_INDEX = 4;
@@ -135,6 +140,7 @@ public class ArBillServiceImpl implements IArBillService {
*/ */
private static final int TEMPLATE_DETAIL_STYLE_INDEX = 31; private static final int TEMPLATE_DETAIL_STYLE_INDEX = 31;
private static final String TEMPLATE_SHEET_XML = "xl/worksheets/sheet1.xml"; private static final String TEMPLATE_SHEET_XML = "xl/worksheets/sheet1.xml";
private static final String TEMPLATE_STYLES_XML = "xl/styles.xml";
private static final Pattern TEMPLATE_COLUMN_PATTERN = Pattern.compile("<col\\b([^>]*)/\\s*>", Pattern.DOTALL); private static final Pattern TEMPLATE_COLUMN_PATTERN = Pattern.compile("<col\\b([^>]*)/\\s*>", Pattern.DOTALL);
private static final Pattern TEMPLATE_COLUMN_MIN_PATTERN = Pattern.compile("\\bmin\\s*=\\s*[\"'](\\d+)[\"']"); private static final Pattern TEMPLATE_COLUMN_MIN_PATTERN = Pattern.compile("\\bmin\\s*=\\s*[\"'](\\d+)[\"']");
private static final Pattern TEMPLATE_COLUMN_MAX_PATTERN = Pattern.compile("\\bmax\\s*=\\s*[\"'](\\d+)[\"']"); private static final Pattern TEMPLATE_COLUMN_MAX_PATTERN = Pattern.compile("\\bmax\\s*=\\s*[\"'](\\d+)[\"']");
@@ -682,8 +688,8 @@ public class ArBillServiceImpl implements IArBillService {
// 先抓取本批次的未出账费用快照,后续只更新和复制这些费用,避免把同订单已有 BILLED 费用重复带入新账单。 // 先抓取本批次的未出账费用快照,后续只更新和复制这些费用,避免把同订单已有 BILLED 费用重复带入新账单。
List<PrcIncomeBillFee> sourceFees = sourceFeeMapper.selectUnbilledByBillIds( List<PrcIncomeBillFee> sourceFees = sourceFeeMapper.selectUnbilledByBillIds(
currentSourceBillIds, ArIncomeBillFeeStatus.UNBILLED.getCode()); currentSourceBillIds, ArIncomeBillFeeStatus.UNBILLED.getCode());
if (sourceFees == null || sourceFees.isEmpty()) { if (sourceFees == null) {
throw new ServiceException("当前批次没有可生成的未出账费用"); sourceFees = new ArrayList<>();
} }
Map<String, Integer> sourceFeeCounts = new HashMap<>(); Map<String, Integer> sourceFeeCounts = new HashMap<>();
List<String> sourceFeeIds = new ArrayList<>(sourceFees.size()); List<String> sourceFeeIds = new ArrayList<>(sourceFees.size());
@@ -691,9 +697,12 @@ public class ArBillServiceImpl implements IArBillService {
sourceFeeCounts.merge(sourceFee.getBillId(), 1, Integer::sum); sourceFeeCounts.merge(sourceFee.getBillId(), 1, Integer::sum);
sourceFeeIds.add(sourceFee.getFeeId()); sourceFeeIds.add(sourceFee.getFeeId());
} }
for (String sourceBillId : currentSourceBillIds) { for (PrcOrderBill source : sources) {
if (!sourceFeeCounts.containsKey(sourceBillId)) { BillBillingFailStatus billingFailStatus = BillBillingFailStatus.of(source.getBillingFailStatus());
throw new ServiceException("来源订单没有可生成的未出账费用:" + sourceBillId); if (!sourceFeeCounts.containsKey(source.getBillId())
&& (billingFailStatus == null || billingFailStatus == BillBillingFailStatus.SUCCESS
|| nvl(source.getIncomeTotal()).compareTo(BigDecimal.ZERO) != 0)) {
throw new ServiceException("来源订单没有可生成的未出账费用:" + source.getBillId());
} }
} }
@@ -702,12 +711,14 @@ public class ArBillServiceImpl implements IArBillService {
if (claimedCount != currentSourceBillIds.size()) { if (claimedCount != currentSourceBillIds.size()) {
throw new ServiceException("部分计费订单已被其他账单占用,请重新查询后生成"); throw new ServiceException("部分计费订单已被其他账单占用,请重新查询后生成");
} }
int billedFeeCount = sourceFeeMapper.updateArBillStatusByFeeIds(sourceFeeIds, // 纯失败订单批次没有费用,不能执行空 IN 更新。
ArIncomeBillFeeStatus.UNBILLED.getCode(), ArIncomeBillFeeStatus.BILLED.getCode()); if (!sourceFeeIds.isEmpty()) {
if (billedFeeCount != sourceFeeIds.size()) { int billedFeeCount = sourceFeeMapper.updateArBillStatusByFeeIds(sourceFeeIds,
throw new ServiceException("部分费用状态已发生变化,请重新查询后生成"); 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; int detailNo = (task.getProcessedCount() == null ? 0 : task.getProcessedCount()) + 1;
List<ArBillDetail> details = new ArrayList<>(sources.size()); List<ArBillDetail> details = new ArrayList<>(sources.size());
@@ -716,6 +727,8 @@ public class ArBillServiceImpl implements IArBillService {
detailNo++, task.getCreateBy())); detailNo++, task.getCreateBy()));
} }
detailMapper.insertBatch(details); detailMapper.insertBatch(details);
// 失败订单先落快照,再汇总占用状态,避免无费用订单被错误恢复为 UNBILLED。
sourceBillMapper.refreshArBillStatusByBillIds(currentSourceBillIds);
Map<String, ArBillDetail> detailsBySourceBill = new HashMap<>(); Map<String, ArBillDetail> detailsBySourceBill = new HashMap<>();
for (ArBillDetail detail : detailMapper.selectByBillIdAndSourceIds(bill.getArBillId(), currentSourceBillIds)) { for (ArBillDetail detail : detailMapper.selectByBillIdAndSourceIds(bill.getArBillId(), currentSourceBillIds)) {
@@ -1165,6 +1178,7 @@ public class ArBillServiceImpl implements IArBillService {
detail.setProductCode(source.getProductCode()); detail.setProductCode(source.getProductCode());
detail.setProductName(source.getProductName()); detail.setProductName(source.getProductName());
detail.setGoodsDescription(source.getOrderRemark()); detail.setGoodsDescription(source.getOrderRemark());
detail.setBillingFailStatus(source.getBillingFailStatus());
detail.setCountryCode(source.getCountryCode()); detail.setCountryCode(source.getCountryCode());
detail.setCountryName(source.getCountryName()); detail.setCountryName(source.getCountryName());
detail.setDestPostcode(source.getDestPostcode()); detail.setDestPostcode(source.getDestPostcode());
@@ -1262,9 +1276,16 @@ public class ArBillServiceImpl implements IArBillService {
} }
/** /**
* 复制模板 ZIP 包,仅替换 sheet1.xml,保持模板样式、合并单元格和其他资源不变。 * 复制模板 ZIP 包,替换明细工作表并追加黄色明细样式,保留模板其他资源。
*/ */
private void writeOoxmlWorkbook(ArBill bill, ArExportTemplate template, Path target) throws IOException { private void writeOoxmlWorkbook(ArBill bill, ArExportTemplate template, Path target) throws IOException {
// 样式表单独读取;不加载整张工作簿,且不依赖 styles.xml 和 sheet1.xml 在 ZIP 中的先后顺序。
StylesTable styles = readExportStyles(template);
XSSFCellStyle failedDetailStyle = styles.createCellStyle();
failedDetailStyle.cloneStyleFrom(styles.getStyleAt(TEMPLATE_DETAIL_STYLE_INDEX));
failedDetailStyle.setFillForegroundColor(IndexedColors.YELLOW.getIndex());
failedDetailStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
int failedDetailStyleIndex = Short.toUnsignedInt(failedDetailStyle.getIndex());
try (InputStream input = openTemplateStream(template); ZipInputStream templateZip = new ZipInputStream(input); try (InputStream input = openTemplateStream(template); ZipInputStream templateZip = new ZipInputStream(input);
OutputStream fileOutput = Files.newOutputStream(target); ZipOutputStream output = new ZipOutputStream(fileOutput)) { OutputStream fileOutput = Files.newOutputStream(target); ZipOutputStream output = new ZipOutputStream(fileOutput)) {
boolean replaced = false; boolean replaced = false;
@@ -1272,8 +1293,10 @@ public class ArBillServiceImpl implements IArBillService {
ZipEntry outputEntry = new ZipEntry(entry.getName()); ZipEntry outputEntry = new ZipEntry(entry.getName());
output.putNextEntry(outputEntry); output.putNextEntry(outputEntry);
if (TEMPLATE_SHEET_XML.equals(entry.getName())) { if (TEMPLATE_SHEET_XML.equals(entry.getName())) {
writeOoxmlSheet(readUtf8(templateZip), bill, output); writeOoxmlSheet(readUtf8(templateZip), bill, output, failedDetailStyleIndex);
replaced = true; replaced = true;
} else if (TEMPLATE_STYLES_XML.equals(entry.getName())) {
styles.writeTo(output);
} else if (!entry.isDirectory()) { } else if (!entry.isDirectory()) {
templateZip.transferTo(output); templateZip.transferTo(output);
} }
@@ -1286,6 +1309,22 @@ public class ArBillServiceImpl implements IArBillService {
} }
} }
private StylesTable readExportStyles(ArExportTemplate template) throws IOException {
try (InputStream input = openTemplateStream(template); ZipInputStream templateZip = new ZipInputStream(input)) {
for (ZipEntry entry; (entry = templateZip.getNextEntry()) != null; ) {
if (TEMPLATE_STYLES_XML.equals(entry.getName())) {
StylesTable styles = new StylesTable(templateZip);
if (styles.getNumCellStyles() <= TEMPLATE_DETAIL_STYLE_INDEX) {
throw new IOException("应收账单导出模板缺少明细样式: " + TEMPLATE_DETAIL_STYLE_INDEX);
}
return styles;
}
templateZip.closeEntry();
}
}
throw new IOException("应收账单导出模板缺少样式表: " + TEMPLATE_STYLES_XML);
}
private InputStream openTemplateStream(ArExportTemplate template) throws IOException { private InputStream openTemplateStream(ArExportTemplate template) throws IOException {
String storageType = StringUtils.isEmpty(template.getStorageType()) ? "CLASSPATH" : template.getStorageType(); String storageType = StringUtils.isEmpty(template.getStorageType()) ? "CLASSPATH" : template.getStorageType();
String storageKey = StringUtils.isEmpty(template.getStorageKey()) ? EXPORT_TEMPLATE_RESOURCE : template.getStorageKey(); String storageKey = StringUtils.isEmpty(template.getStorageKey()) ? EXPORT_TEMPLATE_RESOURCE : template.getStorageKey();
@@ -1313,7 +1352,8 @@ public class ArBillServiceImpl implements IArBillService {
/** /**
* 以 XML 流方式写入表头、分页明细和银行信息,避免 POI 在大数据量下移动行。 * 以 XML 流方式写入表头、分页明细和银行信息,避免 POI 在大数据量下移动行。
*/ */
private void writeOoxmlSheet(String templateXml, ArBill bill, OutputStream output) throws IOException { private void writeOoxmlSheet(String templateXml, ArBill bill, OutputStream output, int failedDetailStyleIndex)
throws IOException {
int sheetDataStart = templateXml.indexOf("<sheetData"); int sheetDataStart = templateXml.indexOf("<sheetData");
int sheetDataOpenEnd = templateXml.indexOf('>', sheetDataStart); int sheetDataOpenEnd = templateXml.indexOf('>', sheetDataStart);
int sheetDataEnd = templateXml.indexOf("</sheetData>", sheetDataOpenEnd); int sheetDataEnd = templateXml.indexOf("</sheetData>", sheetDataOpenEnd);
@@ -1332,7 +1372,7 @@ public class ArBillServiceImpl implements IArBillService {
writeUtf8(output, headerRow); writeUtf8(output, headerRow);
writeUtf8(output, templateRows.get(2)); writeUtf8(output, templateRows.get(2));
String customerRow = replaceCellXml(templateRows.get(3), "C3", bill.getCustomerName(), false); String customerRow = replaceCellXml(templateRows.get(3), "C3", bill.getCustomerName(), false);
// 账单所属期区域为 O3:T3 合并单元格,必须写入左上角 O3 才能显示。 // 账单所属期区域为 O3:U3 合并单元格,必须写入左上角 O3 才能显示。
customerRow = replaceCellXml(customerRow, "O3", exportReceiveTimeRange(receiveTimeRange), false); customerRow = replaceCellXml(customerRow, "O3", exportReceiveTimeRange(receiveTimeRange), false);
writeUtf8(output, customerRow); writeUtf8(output, customerRow);
writeUtf8(output, templateRows.get(4)); writeUtf8(output, templateRows.get(4));
@@ -1346,7 +1386,7 @@ public class ArBillServiceImpl implements IArBillService {
} }
attachFees(details); attachFees(details);
for (ArBillDetail detail : details) { for (ArBillDetail detail : details) {
writeOoxmlDetailRow(output, rowIndex++, detail); writeOoxmlDetailRow(output, rowIndex++, detail, failedDetailStyleIndex);
} }
lastDetailNo = details.get(details.size() - 1).getDetailNo(); lastDetailNo = details.get(details.size() - 1).getDetailNo();
} }
@@ -1490,7 +1530,10 @@ public class ArBillServiceImpl implements IArBillService {
} }
} }
private void writeOoxmlDetailRow(OutputStream output, int rowNumber, ArBillDetail detail) throws IOException { private void writeOoxmlDetailRow(OutputStream output, int rowNumber, ArBillDetail detail, int failedDetailStyleIndex)
throws IOException {
String failureDescription = exportBillingFailDescription(detail.getBillingFailStatus());
int styleIndex = failureDescription.isEmpty() ? TEMPLATE_DETAIL_STYLE_INDEX : failedDetailStyleIndex;
Object[] values = { Object[] values = {
detail.getDetailNo(), detail.getDetailNo(),
detail.getReceiveTime() == null ? "" : new SimpleDateFormat("yyyy-MM-dd").format(detail.getReceiveTime()), detail.getReceiveTime() == null ? "" : new SimpleDateFormat("yyyy-MM-dd").format(detail.getReceiveTime()),
@@ -1500,19 +1543,33 @@ public class ArBillServiceImpl implements IArBillService {
detail.getDestPostcode(), decimal(detail.getReceiveWeight(), 3), decimal(detail.getPkgLength(), 2), detail.getDestPostcode(), decimal(detail.getReceiveWeight(), 3), decimal(detail.getPkgLength(), 2),
decimal(detail.getPkgHeight(), 2), decimal(detail.getPkgWidth(), 2), decimal(detail.getVolumeWeight(), 3), decimal(detail.getPkgHeight(), 2), decimal(detail.getPkgWidth(), 2), decimal(detail.getVolumeWeight(), 3),
decimal(detail.getChargeWeight(), 3), exportUnitPrice(detail), exportProcessingFee(detail), decimal(detail.getChargeWeight(), 3), exportUnitPrice(detail), exportProcessingFee(detail),
exportFormula(detail), exportFeeType(), detail.getCurrency(), detail.getTotalAmount(), exportRemark(detail) exportFormula(detail), exportFeeType(), detail.getCurrency(), detail.getTotalAmount(), exportRemark(detail),
failureDescription
}; };
StringBuilder row = new StringBuilder(2048); StringBuilder row = new StringBuilder(2048);
row.append("<row r=\"").append(rowNumber).append("\" ht=\"24.0\" customHeight=\"true\" spans=\"1:20\">"); row.append("<row r=\"").append(rowNumber).append("\" ht=\"24.0\" customHeight=\"true\" spans=\"1:21\">");
for (int index = 0; index < TEMPLATE_COLUMN_COUNT; index++) { for (int index = 0; index < TEMPLATE_COLUMN_COUNT; index++) {
appendOoxmlCell(row, (char) ('A' + index) + String.valueOf(rowNumber), values[index]); appendOoxmlCell(row, (char) ('A' + index) + String.valueOf(rowNumber), values[index], styleIndex);
} }
row.append("</row>"); row.append("</row>");
writeUtf8(output, row.toString()); writeUtf8(output, row.toString());
} }
private void appendOoxmlCell(StringBuilder row, String reference, Object value) { private String exportBillingFailDescription(Integer billingFailStatus) {
row.append("<c r=\"").append(reference).append("\" s=\"").append(TEMPLATE_DETAIL_STYLE_INDEX).append("\""); BillBillingFailStatus status = BillBillingFailStatus.of(billingFailStatus);
if (status == null) {
return "";
}
return switch (status) {
case PRODUCT -> "未匹配产品";
case COUNTRY -> "未匹配国家";
case WEIGHT -> "未匹配重量段";
case SUCCESS -> "";
};
}
private void appendOoxmlCell(StringBuilder row, String reference, Object value, int styleIndex) {
row.append("<c r=\"").append(reference).append("\" s=\"").append(styleIndex).append("\"");
if (value instanceof Number) { if (value instanceof Number) {
row.append(" t=\"n\"><v>").append(numberText((Number) value)).append("</v></c>"); row.append(" t=\"n\"><v>").append(numberText((Number) value)).append("</v></c>");
} else { } else {
@@ -78,10 +78,10 @@ public interface PrcOrderBillMapper
int releaseArBillOrdersByArBillId(@Param("arBillId") Long arBillId, @Param("operator") String operator, int releaseArBillOrdersByArBillId(@Param("arBillId") Long arBillId, @Param("operator") String operator,
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus); @Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
/** 根据费用明细状态汇总订单的 UNBILLED/PARTIAL/BILLED 状态。 */ /** 根据费用明细状态及无费用失败订单的有效账单快照汇总订单出账状态。 */
int refreshArBillStatusByBillIds(@Param("billIds") List<String> billIds); int refreshArBillStatusByBillIds(@Param("billIds") List<String> billIds);
/** 按应收账单关联的来源订单批量汇总状态,适用于大账单清理和作废。 */ /** 清理或作废时批量释放来源订单;无费用失败订单的占用判断排除当前账单。 */
int refreshArBillStatusByArBillId(@Param("arBillId") Long arBillId); int refreshArBillStatusByArBillId(@Param("arBillId") Long arBillId);
public int insertPrcOrderBill(PrcOrderBill bill); public int insertPrcOrderBill(PrcOrderBill bill);
@@ -16,6 +16,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="productCode" column="product_code"/> <result property="productCode" column="product_code"/>
<result property="productName" column="product_name"/> <result property="productName" column="product_name"/>
<result property="goodsDescription" column="goods_description"/> <result property="goodsDescription" column="goods_description"/>
<result property="billingFailStatus" column="billing_fail_status"/>
<result property="countryCode" column="country_code"/> <result property="countryCode" column="country_code"/>
<result property="countryName" column="country_name"/> <result property="countryName" column="country_name"/>
<result property="destPostcode" column="dest_postcode"/> <result property="destPostcode" column="dest_postcode"/>
@@ -41,7 +42,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time, select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
product_code, product_name, goods_description, country_code, country_name, dest_postcode, product_code, product_name, goods_description, country_code, country_name, dest_postcode,
actual_weight, receive_weight, volume_weight, charge_weight, freight_rate, registration_fee, pkg_length, pkg_width, pkg_height, actual_weight, receive_weight, volume_weight, charge_weight, freight_rate, registration_fee, pkg_length, pkg_width, pkg_height,
currency, total_amount, product_amount, redline_amount, remark, create_by, create_time currency, total_amount, product_amount, redline_amount, billing_fail_status, remark, create_by, create_time
from ar_bill_detail where ar_bill_id = #{arBillId} order by detail_no from ar_bill_detail where ar_bill_id = #{arBillId} order by detail_no
</select> </select>
@@ -49,7 +50,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time, select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
product_code, product_name, goods_description, country_code, country_name, dest_postcode, product_code, product_name, goods_description, country_code, country_name, dest_postcode,
actual_weight, receive_weight, volume_weight, charge_weight, freight_rate, registration_fee, pkg_length, pkg_width, pkg_height, actual_weight, receive_weight, volume_weight, charge_weight, freight_rate, registration_fee, pkg_length, pkg_width, pkg_height,
currency, total_amount, product_amount, redline_amount, remark, create_by, create_time currency, total_amount, product_amount, redline_amount, billing_fail_status, remark, create_by, create_time
from ar_bill_detail from ar_bill_detail
where ar_bill_id = #{arBillId} where ar_bill_id = #{arBillId}
and prc_order_bill_id in and prc_order_bill_id in
@@ -87,7 +88,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select d.detail_id, d.ar_bill_id, d.prc_order_bill_id, d.detail_no, d.order_no, d.tracking_no, d.transfer_no, d.receive_time, select d.detail_id, d.ar_bill_id, d.prc_order_bill_id, d.detail_no, d.order_no, d.tracking_no, d.transfer_no, d.receive_time,
d.product_code, d.product_name, d.goods_description, d.country_code, d.country_name, d.dest_postcode, d.product_code, d.product_name, d.goods_description, d.country_code, d.country_name, d.dest_postcode,
d.actual_weight, d.receive_weight, d.volume_weight, d.charge_weight, d.freight_rate, d.registration_fee, d.pkg_length, d.pkg_width, d.pkg_height, d.actual_weight, d.receive_weight, d.volume_weight, d.charge_weight, d.freight_rate, d.registration_fee, d.pkg_length, d.pkg_width, d.pkg_height,
d.currency, d.total_amount, d.product_amount, d.redline_amount, d.remark, d.create_by, d.create_time, d.currency, d.total_amount, d.product_amount, d.redline_amount, d.billing_fail_status, d.remark, d.create_by, d.create_time,
f.detail_fee_id as fee_detail_id, f.detail_id as fee_parent_detail_id, f.detail_fee_id as fee_detail_id, f.detail_id as fee_parent_detail_id,
f.prc_income_bill_fee_id as fee_prc_income_bill_fee_id, f.prc_income_bill_fee_id as fee_prc_income_bill_fee_id,
f.fee_no, f.fee_type, f.fee_name, f.sc_id as fee_sc_id, f.quantity as fee_quantity, f.fee_no, f.fee_type, f.fee_name, f.sc_id as fee_sc_id, f.quantity as fee_quantity,
@@ -106,7 +107,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time, select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
product_code, product_name, goods_description, country_code, country_name, dest_postcode, product_code, product_name, goods_description, country_code, country_name, dest_postcode,
actual_weight, receive_weight, volume_weight, charge_weight, freight_rate, registration_fee, pkg_length, pkg_width, pkg_height, actual_weight, receive_weight, volume_weight, charge_weight, freight_rate, registration_fee, pkg_length, pkg_width, pkg_height,
currency, total_amount, product_amount, redline_amount, remark, create_by, create_time currency, total_amount, product_amount, redline_amount, billing_fail_status, remark, create_by, create_time
from ar_bill_detail from ar_bill_detail
where ar_bill_id = #{arBillId} and detail_no &gt; #{lastDetailNo} where ar_bill_id = #{arBillId} and detail_no &gt; #{lastDetailNo}
order by detail_no order by detail_no
@@ -125,12 +126,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
product_name, goods_description, country_code, country_name, dest_postcode, product_name, goods_description, country_code, country_name, dest_postcode,
actual_weight, receive_weight, actual_weight, receive_weight,
volume_weight, charge_weight, freight_rate, registration_fee, pkg_length, pkg_width, pkg_height, currency, volume_weight, charge_weight, freight_rate, registration_fee, pkg_length, pkg_width, pkg_height, currency,
total_amount, product_amount, redline_amount, total_amount, product_amount, redline_amount, billing_fail_status,
remark, create_by, create_time) remark, create_by, create_time)
values (#{arBillId}, #{prcOrderBillId}, #{detailNo}, #{orderNo}, #{trackingNo}, #{transferNo}, #{receiveTime}, values (#{arBillId}, #{prcOrderBillId}, #{detailNo}, #{orderNo}, #{trackingNo}, #{transferNo}, #{receiveTime},
#{productCode}, #{productName}, #{goodsDescription}, #{countryCode}, #{countryName}, #{productCode}, #{productName}, #{goodsDescription}, #{countryCode}, #{countryName},
#{destPostcode}, #{actualWeight}, #{receiveWeight}, #{volumeWeight}, #{chargeWeight}, #{freightRate}, #{registrationFee}, #{pkgLength}, #{pkgWidth}, #{destPostcode}, #{actualWeight}, #{receiveWeight}, #{volumeWeight}, #{chargeWeight}, #{freightRate}, #{registrationFee}, #{pkgLength}, #{pkgWidth},
#{pkgHeight}, #{currency}, #{totalAmount}, #{productAmount}, #{redlineAmount}, #{remark}, #{createBy}, sysdate()) #{pkgHeight}, #{currency}, #{totalAmount}, #{productAmount}, #{redlineAmount}, #{billingFailStatus}, #{remark}, #{createBy}, sysdate())
</insert> </insert>
<insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="detailId"> <insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="detailId">
@@ -138,7 +139,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time, product_code, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time, product_code,
product_name, goods_description, country_code, country_name, dest_postcode, actual_weight, receive_weight, product_name, goods_description, country_code, country_name, dest_postcode, actual_weight, receive_weight,
volume_weight, charge_weight, freight_rate, registration_fee, pkg_length, pkg_width, pkg_height, currency, total_amount, volume_weight, charge_weight, freight_rate, registration_fee, pkg_length, pkg_width, pkg_height, currency, total_amount,
product_amount, redline_amount, product_amount, redline_amount, billing_fail_status,
remark, create_by, create_time remark, create_by, create_time
) values ) values
<foreach collection="list" item="item" separator=","> <foreach collection="list" item="item" separator=",">
@@ -146,7 +147,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{item.transferNo}, #{item.receiveTime}, #{item.productCode}, #{item.productName}, #{item.goodsDescription}, #{item.transferNo}, #{item.receiveTime}, #{item.productCode}, #{item.productName}, #{item.goodsDescription},
#{item.countryCode}, #{item.countryName}, #{item.destPostcode}, #{item.actualWeight}, #{item.receiveWeight}, #{item.countryCode}, #{item.countryName}, #{item.destPostcode}, #{item.actualWeight}, #{item.receiveWeight},
#{item.volumeWeight}, #{item.chargeWeight}, #{item.freightRate}, #{item.registrationFee}, #{item.pkgLength}, #{item.pkgWidth}, #{item.pkgHeight}, #{item.volumeWeight}, #{item.chargeWeight}, #{item.freightRate}, #{item.registrationFee}, #{item.pkgLength}, #{item.pkgWidth}, #{item.pkgHeight},
#{item.currency}, #{item.totalAmount}, #{item.productAmount}, #{item.redlineAmount}, #{item.currency}, #{item.totalAmount}, #{item.productAmount}, #{item.redlineAmount}, #{item.billingFailStatus},
#{item.remark}, #{item.createBy}, sysdate()) #{item.remark}, #{item.createBy}, sysdate())
</foreach> </foreach>
</insert> </insert>
@@ -118,7 +118,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where f.bill_id = p.bill_id where f.bill_id = p.bill_id
and f.ar_bill_status = #{feeArBillStatus}) as income_total, and f.ar_bill_status = #{feeArBillStatus}) as income_total,
p.pay_total, p.currency, p.ar_bill_status, p.pay_total, p.currency, p.ar_bill_status,
p.customer_active_status, p.credit_arrears_status, p.refund_status, p.round_mode, p.customer_active_status, p.credit_arrears_status, p.refund_status, p.billing_fail_status, p.round_mode,
p.created_by, p.create_time, p.updated_by, p.update_time p.created_by, p.create_time, p.updated_by, p.update_time
from prc_order_bill p from prc_order_bill p
left join prc_order_bill_ext e on e.bill_id = p.bill_id left join prc_order_bill_ext e on e.bill_id = p.bill_id
@@ -358,6 +358,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</if> </if>
</sql> </sql>
<!-- 有未出账费用的订单,或尚未纳入有效账单的无费用计费失败订单。 -->
<sql id="arBillEligibleSourceCondition">
and (
exists (
select 1 from prc_income_bill_fee f
where f.bill_id = p.bill_id and f.ar_bill_status = #{feeArBillStatus}
)
or (
p.billing_fail_status in (1, 2, 3)
and not exists (
select 1 from prc_income_bill_fee f where f.bill_id = p.bill_id
)
and not exists (
select 1 from ar_bill_detail d
inner join ar_bill b on b.ar_bill_id = d.ar_bill_id
where d.prc_order_bill_id = p.bill_id
and d.billing_fail_status in (1, 2, 3)
and b.status &lt;&gt; 'VOID' and b.generation_status &lt;&gt; 'FAILED'
)
)
)
</sql>
<select id="selectPrcOrderBillByCustomerPeriod" resultMap="PrcOrderBillArSourceResult"> <select id="selectPrcOrderBillByCustomerPeriod" resultMap="PrcOrderBillArSourceResult">
<include refid="selectArBillSourceVo"/> <include refid="selectArBillSourceVo"/>
where p.customer_id = #{customerId} where p.customer_id = #{customerId}
@@ -385,11 +408,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<foreach collection="arBillStatuses" item="status" open="(" separator="," close=")"> <foreach collection="arBillStatuses" item="status" open="(" separator="," close=")">
#{status} #{status}
</foreach> </foreach>
and exists ( <include refid="arBillEligibleSourceCondition"/>
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 != ''"> <if test="currency != null and currency != ''">
and p.currency = #{currency} and p.currency = #{currency}
</if> </if>
@@ -448,11 +467,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<foreach collection="arBillStatuses" item="status" open="(" separator="," close=")"> <foreach collection="arBillStatuses" item="status" open="(" separator="," close=")">
#{status} #{status}
</foreach> </foreach>
and exists ( <include refid="arBillEligibleSourceCondition"/>
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 != ''"> <if test="currency != null and currency != ''">
and p.currency = #{currency} and p.currency = #{currency}
</if> </if>
@@ -499,11 +514,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<foreach collection="arBillStatuses" item="status" open="(" separator="," close=")"> <foreach collection="arBillStatuses" item="status" open="(" separator="," close=")">
#{status} #{status}
</foreach> </foreach>
and exists ( <include refid="arBillEligibleSourceCondition"/>
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 and p.bill_id in
<foreach collection="billIds" item="billId" open="(" separator="," close=")"> <foreach collection="billIds" item="billId" open="(" separator="," close=")">
#{billId} #{billId}
@@ -662,7 +673,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and p.ar_bill_status = #{fromStatus} and p.ar_bill_status = #{fromStatus}
</update> </update>
<!-- 订单状态由来源费用状态汇总,避免补录或删除费用后状态失真。 --> <!-- 订单状态按费用汇总;无费用的失败订单以有效账单中的失败快照判断占用。 -->
<update id="refreshArBillStatusByBillIds"> <update id="refreshArBillStatusByBillIds">
update prc_order_bill o update prc_order_bill o
left join ( left join (
@@ -678,7 +689,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
group by bill_id group by bill_id
) f on f.bill_id = o.bill_id ) f on f.bill_id = o.bill_id
set o.ar_bill_status = case set o.ar_bill_status = case
when f.bill_id is null then 'UNBILLED' when f.bill_id is null then case when exists (
select 1 from ar_bill_detail d
inner join ar_bill b on b.ar_bill_id = d.ar_bill_id
where d.prc_order_bill_id = o.bill_id
and d.billing_fail_status in (1, 2, 3)
and b.status &lt;&gt; 'VOID' and b.generation_status &lt;&gt; 'FAILED'
) then 'BILLED' else 'UNBILLED' end
when f.unbilled_count > 0 and f.billed_count > 0 then 'PARTIAL' when f.unbilled_count > 0 and f.billed_count > 0 then 'PARTIAL'
when f.unbilled_count > 0 then 'UNBILLED' when f.unbilled_count > 0 then 'UNBILLED'
when f.billed_count = f.fee_count then 'BILLED' when f.billed_count = f.fee_count then 'BILLED'
@@ -712,7 +729,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
group by f.bill_id group by f.bill_id
) fee_summary on fee_summary.bill_id = o.bill_id ) fee_summary on fee_summary.bill_id = o.bill_id
set o.ar_bill_status = case set o.ar_bill_status = case
when fee_summary.bill_id is null then 'UNBILLED' when fee_summary.bill_id is null then case when exists (
select 1 from ar_bill_detail d
inner join ar_bill b on b.ar_bill_id = d.ar_bill_id
where d.prc_order_bill_id = o.bill_id
and d.billing_fail_status in (1, 2, 3)
and b.status &lt;&gt; 'VOID' and b.generation_status &lt;&gt; 'FAILED'
and b.ar_bill_id &lt;&gt; #{arBillId}
) then 'BILLED' else 'UNBILLED' end
when fee_summary.unbilled_count > 0 and fee_summary.billed_count > 0 then 'PARTIAL' 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.unbilled_count > 0 then 'UNBILLED'
when fee_summary.billed_count = fee_summary.fee_count then 'BILLED' when fee_summary.billed_count = fee_summary.fee_count then 'BILLED'
@@ -0,0 +1,21 @@
-- 应收账单明细保存生成时的计费失败状态,导出说明和黄色标注均读取此快照。
-- 历史账单首次补齐为来源订单的当前状态;已有快照不覆盖,找不到来源时保留 NULL。
SET @column_exists := (
SELECT COUNT(1) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ar_bill_detail'
AND COLUMN_NAME = 'billing_fail_status'
);
SET @sql := IF(@column_exists = 0,
'ALTER TABLE `ar_bill_detail` ADD COLUMN `billing_fail_status` tinyint DEFAULT NULL COMMENT ''计费失败状态快照:0成功/1未匹配产品/2未匹配国家/3未匹配重量段'' AFTER `redline_amount`',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE `ar_bill_detail` d
INNER JOIN `prc_order_bill` p ON p.`bill_id` = d.`prc_order_bill_id`
SET d.`billing_fail_status` = p.`billing_fail_status`
WHERE d.`billing_fail_status` IS NULL AND p.`billing_fail_status` IS NOT NULL;
+1
View File
@@ -128,6 +128,7 @@ CREATE TABLE `ar_bill_detail` (
`total_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '订单费用合计', `total_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '订单费用合计',
`product_amount` decimal(18,4) DEFAULT NULL COMMENT '产品价合计快照', `product_amount` decimal(18,4) DEFAULT NULL COMMENT '产品价合计快照',
`redline_amount` decimal(18,4) DEFAULT NULL COMMENT '红线价合计快照', `redline_amount` decimal(18,4) DEFAULT NULL COMMENT '红线价合计快照',
`billing_fail_status` tinyint DEFAULT NULL COMMENT '计费失败状态快照:0成功/1未匹配产品/2未匹配国家/3未匹配重量段',
`remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '订单账单备注', `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '订单账单备注',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '创建人', `create_by` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间', `create_time` datetime NOT NULL COMMENT '创建时间',