Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2430e344a0 | |||
| 5db27e00ba | |||
| a06962d1dd | |||
| 5f679b5a6e | |||
| 02763f092e |
+17
-27
@@ -152,8 +152,8 @@ public class BillingController extends BaseController
|
||||
{
|
||||
query = new PrcOrderBill();
|
||||
}
|
||||
// Billing list only shows success and the three soft misses.
|
||||
query.setBillingFailStatuses(limitBillingListFailStatus(query.getBillingFailStatuses()));
|
||||
// Billing list only shows successfully billed orders.
|
||||
query.setBillingFailStatuses(limitBillingListFailStatus());
|
||||
query.setBillingFailStatus(null);
|
||||
startPage();
|
||||
List<PrcOrderBill> list = prcOrderBillService.selectBillList(query);
|
||||
@@ -161,7 +161,7 @@ public class BillingController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* Async export billing list by current filters (status limited to 0-3).
|
||||
* Async export billing list by current filters (status limited to success=0).
|
||||
* Requires at least one meaningful filter. Poll task then download.
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:export')")
|
||||
@@ -178,7 +178,7 @@ public class BillingController extends BaseController
|
||||
{
|
||||
return error(MessageUtils.message("price.bill.export.filterRequired"));
|
||||
}
|
||||
query.setBillingFailStatuses(limitBillingListFailStatus(query.getBillingFailStatuses()));
|
||||
query.setBillingFailStatuses(limitBillingListFailStatus());
|
||||
query.setBillingFailStatus(null);
|
||||
return successTask(prcOrderBillService.submitOrderBillExport(
|
||||
PrcOrderBillExportTask.TYPE_BILLING, query));
|
||||
@@ -204,32 +204,13 @@ public class BillingController extends BaseController
|
||||
prcOrderBillService.downloadOrderBillExport(taskId, response);
|
||||
}
|
||||
|
||||
private static List<Integer> limitBillingListFailStatus(List<Integer> requested)
|
||||
private static List<Integer> limitBillingListFailStatus()
|
||||
{
|
||||
// Soft-fail list: success + sales 1/2/3 + product-catalog 23/24/25
|
||||
List<Integer> allowed = Arrays.asList(
|
||||
Integer.valueOf(0), Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3),
|
||||
Integer.valueOf(23), Integer.valueOf(24), Integer.valueOf(25));
|
||||
if (requested == null || requested.isEmpty())
|
||||
{
|
||||
return new ArrayList<Integer>(allowed);
|
||||
}
|
||||
List<Integer> kept = new ArrayList<Integer>();
|
||||
for (Integer status : requested)
|
||||
{
|
||||
if (status != null && allowed.contains(status) && !kept.contains(status))
|
||||
{
|
||||
kept.add(status);
|
||||
}
|
||||
}
|
||||
if (kept.isEmpty())
|
||||
{
|
||||
kept.add(Integer.valueOf(-1));
|
||||
}
|
||||
return kept;
|
||||
// Billing list is success-only (billing_fail_status = 0).
|
||||
return new ArrayList<Integer>(Arrays.asList(Integer.valueOf(0)));
|
||||
}
|
||||
|
||||
/** All prc_order_bill rows. Billing list stays limited to status 0-3. */
|
||||
/** All prc_order_bill rows. Billing list stays limited to status 0. */
|
||||
@PreAuthorize("@ss.hasPermi('price:orderBill:list')")
|
||||
@GetMapping("/orderBill/list")
|
||||
public TableDataInfo orderBillList(PrcOrderBill query)
|
||||
@@ -239,6 +220,15 @@ public class BillingController extends BaseController
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
|
||||
/** Order-list popup: billing fail reason by billId (data-scoped). */
|
||||
@PreAuthorize("@ss.hasPermi('price:orderBill:list')")
|
||||
@GetMapping("/orderBill/{billId}/failReason")
|
||||
public AjaxResult orderBillFailReason(@PathVariable("billId") String billId)
|
||||
{
|
||||
return success(prcOrderBillService.getBillingFailReason(billId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Async export order list by current filters.
|
||||
* Requires at least one meaningful filter (default orderStatuses alone is not enough).
|
||||
|
||||
-48
@@ -217,54 +217,6 @@ public class LabelPackageController extends BaseController
|
||||
return toAjax(payProductService.removeLabelProducts(priceSetId, productIds));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:query')")
|
||||
@GetMapping("/{priceSetId}/channels")
|
||||
public AjaxResult listChannels(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
return success(payProductService.listLabelChannels(priceSetId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:edit')")
|
||||
@Log(title = "打单套餐绑定渠道", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{priceSetId}/channels")
|
||||
public AjaxResult bindChannels(@PathVariable Long priceSetId, @RequestBody Map<String, Object> body)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
List<String> channelIds = new ArrayList<String>();
|
||||
Object rawIds = body == null ? null : body.get("channelIds");
|
||||
if (rawIds instanceof List<?>)
|
||||
{
|
||||
for (Object item : (List<?>) rawIds)
|
||||
{
|
||||
if (item != null && !String.valueOf(item).trim().isEmpty())
|
||||
{
|
||||
channelIds.add(String.valueOf(item).trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
Long productId = null;
|
||||
Object rawProductId = body == null ? null : body.get("productId");
|
||||
if (rawProductId instanceof Number)
|
||||
{
|
||||
productId = ((Number) rawProductId).longValue();
|
||||
}
|
||||
else if (rawProductId != null && !String.valueOf(rawProductId).trim().isEmpty()
|
||||
&& !"null".equalsIgnoreCase(String.valueOf(rawProductId).trim()))
|
||||
{
|
||||
productId = Long.valueOf(String.valueOf(rawProductId).trim());
|
||||
}
|
||||
return success(payProductService.bindLabelChannels(priceSetId, channelIds, productId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:edit')")
|
||||
@Log(title = "打单套餐解绑渠道", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{priceSetId}/channels/{channelId}")
|
||||
public AjaxResult unbindChannel(@PathVariable Long priceSetId, @PathVariable String channelId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
return toAjax(payProductService.unbindLabelChannel(priceSetId, channelId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:list')")
|
||||
@GetMapping("/customer/list")
|
||||
|
||||
@@ -14,6 +14,31 @@ import com.bms.receivable.profit.ArCommissionResult;
|
||||
|
||||
class ArProfitMapperSqlTest
|
||||
{
|
||||
@Test
|
||||
void snapshotCalculationIsScopedToTheGeneratingBill() throws Exception
|
||||
{
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.getTypeAliasRegistry().registerAlias("ArBill", com.bms.receivable.domain.ArBill.class);
|
||||
for (String resource : new String[] {"mapper/receivable/ArBillScopeMapper.xml", "mapper/receivable/ArBillMapper.xml"})
|
||||
{
|
||||
try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource))
|
||||
{
|
||||
new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
|
||||
}
|
||||
}
|
||||
var bound = configuration.getMappedStatement("com.bms.receivable.mapper.ArBillMapper.updateProfitSnapshot")
|
||||
.getBoundSql(Map.of("arBillId", 42L, "shenzhenArea", "C", "guangzhouArea", "GZ"));
|
||||
String sql = bound.getSql();
|
||||
assertTrue(sql.contains("fd.ar_bill_id = ?"));
|
||||
assertTrue(sql.contains("d.ar_bill_id = ?"));
|
||||
assertTrue(sql.contains("b.ar_bill_id = ? and b.generation_status = 'GENERATING'"));
|
||||
assertTrue(sql.contains("f.sales_amount = d.total_amount"));
|
||||
assertTrue(sql.contains("a.detail_total != b.total_amount"));
|
||||
assertTrue(sql.contains("'MISSING_PRICE'"));
|
||||
assertTrue(sql.contains("'CURRENCY_MISMATCH'"));
|
||||
assertTrue(sql.contains("'PRICE_OVERFLOW'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void commissionInReportSummaryUsesBillFiltersAndDataScope() throws Exception
|
||||
{
|
||||
@@ -23,7 +48,7 @@ class ArProfitMapperSqlTest
|
||||
{
|
||||
new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
|
||||
}
|
||||
var statement = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectSummary");
|
||||
var statement = configuration.getMappedStatement("com.bms.receivable.mapper.ArProfitMapper.selectSummary");
|
||||
ArProfitQuery query = new ArProfitQuery();
|
||||
query.setCustomerId(5L);
|
||||
query.setBillNo("AR123");
|
||||
@@ -74,7 +99,7 @@ class ArProfitMapperSqlTest
|
||||
}
|
||||
for (String id : new String[] { "selectBills", "selectSummary", "selectBillDetails", "selectFees" })
|
||||
{
|
||||
String name = "com.bms.ar.mapper.ArProfitMapper." + id;
|
||||
String name = "com.bms.receivable.mapper.ArProfitMapper." + id;
|
||||
String self = configuration.getMappedStatement(name).getBoundSql(Map.of(
|
||||
"query", new ArProfitQuery(), "scope", new ArProfitScope(false, true, false, 8L, 10L))).getSql();
|
||||
String manager = configuration.getMappedStatement(name).getBoundSql(Map.of(
|
||||
@@ -84,18 +109,26 @@ class ArProfitMapperSqlTest
|
||||
assertTrue(manager.contains("find_in_set"));
|
||||
assertTrue(manager.contains("b.status in"));
|
||||
}
|
||||
String bills = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectBills")
|
||||
String bills = configuration.getMappedStatement("com.bms.receivable.mapper.ArProfitMapper.selectBills")
|
||||
.getBoundSql(Map.of("query", new ArProfitQuery(), "scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql();
|
||||
assertTrue(bills.contains("from ar_bill b"));
|
||||
assertTrue(bills.contains("b.bill_name"));
|
||||
assertTrue(bills.contains("b.profit_calc_status = 'READY'"));
|
||||
assertTrue(bills.contains("b.product_amount"));
|
||||
assertTrue(bills.contains("b.redline_amount"));
|
||||
assertFalse(bills.contains("ar_bill_detail"));
|
||||
assertFalse(bills.contains("order_no"));
|
||||
String billCommission = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.selectCommissionSummary")
|
||||
String summary = configuration.getMappedStatement("com.bms.receivable.mapper.ArProfitMapper.selectSummary")
|
||||
.getBoundSql(Map.of("query", new ArProfitQuery(), "scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql();
|
||||
assertFalse(summary.contains("ar_bill_detail"));
|
||||
assertTrue(summary.contains("ar_profit_commission_result"));
|
||||
String billCommission = configuration.getMappedStatement("com.bms.receivable.mapper.ArCommissionMapper.selectCommissionSummary")
|
||||
.getBoundSql(Map.of("billId", 9L, "scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql();
|
||||
assertTrue(billCommission.contains("r.ar_bill_id = ?"));
|
||||
assertTrue(billCommission.contains("then null"));
|
||||
for (String id : new String[] { "selectResults", "selectCommissionSummary" })
|
||||
{
|
||||
var statement = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper." + id);
|
||||
var statement = configuration.getMappedStatement("com.bms.receivable.mapper.ArCommissionMapper." + id);
|
||||
String self = statement.getBoundSql(Map.of("billId", 9L, "includeReversed", true,
|
||||
"scope", new ArProfitScope(false, true, false, 8L, 10L))).getSql();
|
||||
String department = statement.getBoundSql(Map.of("includeReversed", false,
|
||||
@@ -113,7 +146,7 @@ class ArProfitMapperSqlTest
|
||||
assertFalse(all.contains("b.profit_sales_user_id = ?"));
|
||||
assertFalse(all.contains("find_in_set"));
|
||||
}
|
||||
var salesSql = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectSalesOptions")
|
||||
var salesSql = configuration.getMappedStatement("com.bms.receivable.mapper.ArProfitMapper.selectSalesOptions")
|
||||
.getBoundSql(Map.of("keyword", "sales"));
|
||||
assertTrue(salesSql.getSql().contains("from sys_user u"));
|
||||
assertTrue(salesSql.getSql().contains("u.nick_name like"));
|
||||
@@ -121,11 +154,11 @@ class ArProfitMapperSqlTest
|
||||
assertFalse(salesSql.getSql().contains("ar_bill"));
|
||||
assertFalse(salesSql.getSql().contains("sys_role"));
|
||||
assertTrue(salesSql.getSql().contains("limit 50"));
|
||||
String settlement = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.selectSettlementInputs")
|
||||
String settlement = configuration.getMappedStatement("com.bms.receivable.mapper.ArCommissionMapper.selectSettlementInputs")
|
||||
.getBoundSql(Map.of("billId", 1L)).getSql();
|
||||
assertTrue(settlement.contains("w.status = 'EFFECTIVE'"));
|
||||
assertTrue(settlement.contains("b.status = 'WRITTEN_OFF'"));
|
||||
var insert = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.insertResults")
|
||||
var insert = configuration.getMappedStatement("com.bms.receivable.mapper.ArCommissionMapper.insertResults")
|
||||
.getBoundSql(Map.of("items", java.util.List.of(new ArCommissionResult(), new ArCommissionResult()), "operator", "test"));
|
||||
assertTrue(insert.getSql().contains("ar_profit_commission_result"));
|
||||
assertTrue(insert.getParameterMappings().size() == 16);
|
||||
|
||||
@@ -20,9 +20,6 @@ public class ChannelProductMap
|
||||
|
||||
private Long productId;
|
||||
|
||||
/** 打单套餐 ID,对应 prc_price_set.price_set_id(应收可填) */
|
||||
private Long priceSetId;
|
||||
|
||||
/** 渠道名称(列表回显) */
|
||||
private String channelName;
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.bms.price.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Order-list billing fail reason popup payload.
|
||||
*/
|
||||
@Data
|
||||
public class OrderBillFailReasonVo implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String billId;
|
||||
private String orderNo;
|
||||
private String transferNo;
|
||||
private String trackingNo;
|
||||
|
||||
private String customerCode;
|
||||
private String customerName;
|
||||
private String productCode;
|
||||
private String productName;
|
||||
private String countryCode;
|
||||
private String countryName;
|
||||
private String zoneCode;
|
||||
private String zoneName;
|
||||
private String destPostcode;
|
||||
|
||||
private Long priceSetId;
|
||||
private String priceSetCode;
|
||||
private String priceSetName;
|
||||
private String setType;
|
||||
private Long psvId;
|
||||
private String versionNo;
|
||||
private Long psdId;
|
||||
|
||||
private BigDecimal weight;
|
||||
private BigDecimal chargeWeight;
|
||||
private BigDecimal weightMin;
|
||||
private BigDecimal weightMax;
|
||||
private String weightLabel;
|
||||
private BigDecimal freightRate;
|
||||
private BigDecimal freightAmount;
|
||||
private BigDecimal incomeTotal;
|
||||
private String currency;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date bizDate;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date pickupDate;
|
||||
|
||||
private Integer billingFailStatus;
|
||||
private String billingFailStatusLabel;
|
||||
/** Raw persisted fail text. */
|
||||
private String errorMessage;
|
||||
/** Templated human-readable fail reason. */
|
||||
private String failReasonDetail;
|
||||
}
|
||||
@@ -1,24 +1,19 @@
|
||||
package com.bms.price.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.price.domain.ChannelProductMap;
|
||||
|
||||
/**
|
||||
* 渠道产品关系映射
|
||||
* Channel-product pay-type mapping.
|
||||
*/
|
||||
public interface ChannelProductMapMapper
|
||||
{
|
||||
ChannelProductMap selectByChannelAndType(@Param("channelId") String channelId, @Param("payType") String payType);
|
||||
|
||||
List<ChannelProductMap> selectByPriceSetId(@Param("priceSetId") Long priceSetId);
|
||||
|
||||
int upsert(ChannelProductMap row);
|
||||
|
||||
int deleteByChannelAndType(@Param("channelId") String channelId, @Param("payType") String payType);
|
||||
|
||||
int deleteByChannelTypeAndPriceSet(@Param("channelId") String channelId, @Param("payType") String payType,
|
||||
@Param("priceSetId") Long priceSetId);
|
||||
|
||||
int deleteByProductId(@Param("productId") Long productId);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ public interface PayProductTermMapper
|
||||
|
||||
PayProductTerm selectByProductId(Long productId);
|
||||
|
||||
/** 打单产品所属套餐 ID */
|
||||
Long selectLabelPriceSetIdByProduct(@Param("productId") Long productId);
|
||||
|
||||
List<PayProductTerm> selectByPriceSetId(@Param("priceSetId") Long priceSetId);
|
||||
|
||||
|
||||
@@ -91,14 +91,6 @@ public interface IPayProductService
|
||||
/** 从打单套餐移除产品(失效/清理对应 term 明细) */
|
||||
int removeLabelProducts(Long priceSetId, Long[] productIds);
|
||||
|
||||
/** 打单套餐已绑定渠道列表 */
|
||||
java.util.List<ChannelProductMap> listLabelChannels(Long priceSetId);
|
||||
|
||||
/** 打单套餐批量绑定渠道(写 AR + price_set_id) */
|
||||
String bindLabelChannels(Long priceSetId, java.util.List<String> channelIds, Long productId);
|
||||
|
||||
/** 打单套餐解绑渠道 */
|
||||
int unbindLabelChannel(Long priceSetId, String channelId);
|
||||
|
||||
/** 取消渠道上指定收付类型的产品。payType 为空时按应付 AP */
|
||||
void unbindChannel(String channelId, String payType);
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
|
||||
import com.bms.price.domain.vo.ChargeWeightImportRow;
|
||||
import com.bms.price.domain.vo.ChargeWeightTemplateRequest;
|
||||
import com.bms.price.domain.vo.LocalRebillRequest;
|
||||
import com.bms.price.domain.vo.OrderBillFailReasonVo;
|
||||
import com.bms.price.domain.vo.RecalcBillPriceRequest;
|
||||
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
||||
@@ -60,6 +61,9 @@ public interface IPrcOrderBillService
|
||||
|
||||
TmsPriceQuoteResult getBill(String billId);
|
||||
|
||||
/** Billing fail reason for order-list popup (data-scoped). */
|
||||
OrderBillFailReasonVo getBillingFailReason(String billId);
|
||||
|
||||
TmsPriceQuoteResult getBillByOrderNo(String orderNo);
|
||||
|
||||
/** Manual add income fee from surcharge config; refresh receivable totals. */
|
||||
|
||||
-101
@@ -1056,103 +1056,6 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
return n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChannelProductMap> listLabelChannels(Long priceSetId)
|
||||
{
|
||||
if (priceSetId == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ChannelProductMap> list = channelProductMapMapper.selectByPriceSetId(priceSetId);
|
||||
return list == null ? Collections.emptyList() : list;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String bindLabelChannels(Long priceSetId, List<String> channelIds, Long productId)
|
||||
{
|
||||
if (priceSetId == null)
|
||||
{
|
||||
throw new ServiceException("请选择打单套餐");
|
||||
}
|
||||
if (channelIds == null || channelIds.isEmpty())
|
||||
{
|
||||
throw new ServiceException("请选择要绑定的渠道");
|
||||
}
|
||||
if (productId == null)
|
||||
{
|
||||
throw new ServiceException("请选择套餐内应收产品");
|
||||
}
|
||||
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(priceSetId);
|
||||
if (set == null || !PrcPriceSet.CATEGORY_LABEL.equalsIgnoreCase(String.valueOf(set.getSetCategory())))
|
||||
{
|
||||
throw new ServiceException("打单套餐不存在");
|
||||
}
|
||||
Long root = resolveRoot(productId);
|
||||
boolean inSet = false;
|
||||
List<PayProduct> products = selectLabelProducts(priceSetId);
|
||||
for (PayProduct p : products)
|
||||
{
|
||||
if (p != null && root.equals(p.getProductId()))
|
||||
{
|
||||
inSet = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inSet)
|
||||
{
|
||||
throw new ServiceException("所选产品不属于本打单套餐");
|
||||
}
|
||||
String operator = SecurityUtils.getUsername();
|
||||
int n = 0;
|
||||
for (String channelId : channelIds)
|
||||
{
|
||||
if (isBlank(channelId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ServiceChannel channel = serviceChannelMapper.selectServiceChannelById(channelId.trim());
|
||||
if (channel == null || isBlank(channel.getChannelCode()))
|
||||
{
|
||||
throw new ServiceException("渠道不存在或渠道编码为空:" + channelId);
|
||||
}
|
||||
ChannelProductMap row = new ChannelProductMap();
|
||||
row.setChannelId(channelId.trim());
|
||||
row.setChannelCode(channel.getChannelCode());
|
||||
row.setProductId(root);
|
||||
row.setPriceSetId(priceSetId);
|
||||
row.setPayType(PAY_TYPE_AR);
|
||||
row.setCreateBy(operator);
|
||||
row.setUpdateBy(operator);
|
||||
channelProductMapMapper.upsert(row);
|
||||
n++;
|
||||
}
|
||||
if (n <= 0)
|
||||
{
|
||||
throw new ServiceException("没有可绑定的渠道");
|
||||
}
|
||||
return "已绑定 " + n + " 个渠道";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int unbindLabelChannel(Long priceSetId, String channelId)
|
||||
{
|
||||
if (priceSetId == null || isBlank(channelId))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return channelProductMapMapper.deleteByChannelTypeAndPriceSet(channelId.trim(), PAY_TYPE_AR, priceSetId);
|
||||
}
|
||||
|
||||
private Long resolveLabelPriceSetId(Long productId)
|
||||
{
|
||||
if (productId == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return payProductTermMapper.selectLabelPriceSetIdByProduct(productId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PayProduct> selectLabelProducts(Long priceSetId)
|
||||
@@ -1756,10 +1659,6 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
row.setChannelCode(channel.getChannelCode());
|
||||
row.setProductId(storedId);
|
||||
row.setPayType(type);
|
||||
if (PAY_TYPE_AR.equals(type))
|
||||
{
|
||||
row.setPriceSetId(resolveLabelPriceSetId(storedId));
|
||||
}
|
||||
String operator = SecurityUtils.getUsername();
|
||||
row.setCreateBy(operator);
|
||||
row.setUpdateBy(operator);
|
||||
|
||||
+92
@@ -62,6 +62,7 @@ import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
|
||||
import com.bms.price.domain.vo.ChargeWeightImportRow;
|
||||
import com.bms.price.domain.vo.ChargeWeightTemplateRequest;
|
||||
import com.bms.price.domain.vo.LocalRebillRequest;
|
||||
import com.bms.price.domain.vo.OrderBillFailReasonVo;
|
||||
import com.bms.price.domain.vo.RecalcBillPriceRequest;
|
||||
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
|
||||
import com.bms.price.task.PrcBatchAddIncomeFeeAsyncRunner;
|
||||
@@ -70,6 +71,7 @@ import com.bms.price.task.PrcChargeWeightImportAsyncRunner.ChargeWeightImportWor
|
||||
import com.bms.price.task.PrcOrderBillExportAsyncRunner;
|
||||
import com.bms.price.task.PrcOrderBillLocalRebillAsyncRunner;
|
||||
import com.bms.price.task.PrcRecalcBillPriceAsyncRunner;
|
||||
import com.bms.price.service.support.BillingFailReasonSupport;
|
||||
import com.bms.price.service.support.OrderBillExportSupport;
|
||||
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.bms.price.domain.vo.TmsOrderBillStatusResult;
|
||||
@@ -85,6 +87,10 @@ import com.bms.price.mapper.PrcIncomeBillFeeMapper;
|
||||
import com.bms.price.mapper.PrcIncomeBillMapper;
|
||||
import com.bms.price.mapper.PrcOrderBillExtMapper;
|
||||
import com.bms.price.mapper.PrcOrderBillMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetDetailMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetVersionMapper;
|
||||
import com.bms.price.domain.PrcPriceSetDetail;
|
||||
import com.bms.price.domain.PrcPriceSetVersion;
|
||||
import com.bms.price.mapper.PrcPayBillFeeMapper;
|
||||
import com.bms.price.mapper.PrcPayBillMapper;
|
||||
import com.bms.price.oracle.OracleOrderBillExtFiller;
|
||||
@@ -120,6 +126,12 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
@Autowired
|
||||
private PrcOrderBillMapper orderBillMapper;
|
||||
|
||||
@Autowired
|
||||
private PrcPriceSetVersionMapper priceSetVersionMapper;
|
||||
|
||||
@Autowired
|
||||
private PrcPriceSetDetailMapper priceSetDetailMapper;
|
||||
|
||||
/** TMS 实时状态查询使用客户主档案,不读取订单上的历史状态快照。 */
|
||||
@Autowired
|
||||
private BmsCustomerMapper bmsCustomerMapper;
|
||||
@@ -432,6 +444,86 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
payBillFeeMapper.selectByBillId(bill.getBillId()));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public OrderBillFailReasonVo getBillingFailReason(String billId)
|
||||
{
|
||||
if (StringUtils.isEmpty(billId))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.billIdRequired"));
|
||||
}
|
||||
PrcOrderBill query = new PrcOrderBill();
|
||||
query.setBillId(billId.trim());
|
||||
List<PrcOrderBill> list = self.selectBillList(query);
|
||||
if (list == null || list.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.notFound"));
|
||||
}
|
||||
PrcOrderBill bill = list.get(0);
|
||||
OrderBillFailReasonVo vo = new OrderBillFailReasonVo();
|
||||
vo.setBillId(bill.getBillId());
|
||||
vo.setOrderNo(bill.getOrderNo());
|
||||
vo.setTransferNo(bill.getTransferNo());
|
||||
vo.setTrackingNo(bill.getTrackingNo());
|
||||
vo.setCustomerCode(bill.getCustomerCode());
|
||||
vo.setCustomerName(bill.getCustomerName());
|
||||
vo.setProductCode(bill.getProductCode());
|
||||
vo.setProductName(bill.getProductName());
|
||||
vo.setCountryCode(bill.getCountryCode());
|
||||
vo.setCountryName(bill.getCountryName());
|
||||
vo.setDestPostcode(bill.getDestPostcode());
|
||||
vo.setPriceSetId(bill.getPriceSetId());
|
||||
vo.setPriceSetCode(bill.getPriceSetCode());
|
||||
vo.setPriceSetName(bill.getPriceSetName());
|
||||
vo.setSetType(bill.getSetType());
|
||||
vo.setPsvId(bill.getPsvId());
|
||||
vo.setPsdId(bill.getPsdId());
|
||||
vo.setWeight(bill.getWeight());
|
||||
vo.setChargeWeight(bill.getChargeWeight());
|
||||
vo.setWeightMin(bill.getWeightMin());
|
||||
vo.setWeightMax(bill.getWeightMax());
|
||||
vo.setWeightLabel(bill.getWeightLabel());
|
||||
vo.setFreightRate(bill.getFreightRate());
|
||||
vo.setFreightAmount(bill.getFreightAmount());
|
||||
vo.setIncomeTotal(bill.getIncomeTotal());
|
||||
vo.setCurrency(bill.getCurrency());
|
||||
vo.setBizDate(bill.getBizDate());
|
||||
vo.setPickupDate(bill.getPickupDate());
|
||||
Integer status = bill.getBillingFailStatus() == null
|
||||
? Integer.valueOf(BillBillingFailStatus.SUCCESS.getCode()) : bill.getBillingFailStatus();
|
||||
vo.setBillingFailStatus(status);
|
||||
vo.setBillingFailStatusLabel(OrderBillExportSupport.billingFailStatusLabel(status));
|
||||
if (bill.getPsvId() != null && priceSetVersionMapper != null)
|
||||
{
|
||||
PrcPriceSetVersion ver = priceSetVersionMapper.selectPrcPriceSetVersionById(bill.getPsvId());
|
||||
if (ver != null)
|
||||
{
|
||||
vo.setVersionNo(ver.getVersionNo());
|
||||
}
|
||||
}
|
||||
if (bill.getPsdId() != null && priceSetDetailMapper != null)
|
||||
{
|
||||
PrcPriceSetDetail detail = priceSetDetailMapper.selectPrcPriceSetDetailById(bill.getPsdId());
|
||||
if (detail != null)
|
||||
{
|
||||
vo.setZoneCode(detail.getZoneCode());
|
||||
vo.setZoneName(detail.getZoneName());
|
||||
}
|
||||
}
|
||||
String reason = bill.getErrorMessage();
|
||||
if (StringUtils.isEmpty(reason))
|
||||
{
|
||||
reason = bill.getMatchMessage();
|
||||
}
|
||||
if (StringUtils.isEmpty(reason))
|
||||
{
|
||||
reason = vo.getBillingFailStatusLabel();
|
||||
}
|
||||
vo.setErrorMessage(reason);
|
||||
vo.setFailReasonDetail(BillingFailReasonSupport.buildDetail(vo, reason));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TmsPriceQuoteResult getBillByOrderNo(String orderNo)
|
||||
{
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package com.bms.price.service.support;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.price.domain.vo.OrderBillFailReasonVo;
|
||||
|
||||
/**
|
||||
* Build templated billing-fail reason text for order-list popup.
|
||||
*/
|
||||
public final class BillingFailReasonSupport
|
||||
{
|
||||
private BillingFailReasonSupport() {}
|
||||
|
||||
private static final Map<Integer, String> TEMPLATES = new HashMap<Integer, String>();
|
||||
|
||||
static
|
||||
{
|
||||
TEMPLATES.put(Integer.valueOf(1), "\u3010\u9500\u552e\u4ef7\u672a\u5339\u914d\u4ea7\u54c1\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u5ba2\u6237 {customer}\uff0c\u4ea7\u54c1 {product}\uff0c\u56fd\u5bb6 {country}\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u5728\u5ba2\u6237\u9500\u552e\u4ef7\u5957\u9910\u300c{priceSet}\u300d\u4e2d\u672a\u627e\u5230\u8be5\u4ea7\u54c1\uff0c\u8bf7\u68c0\u67e5\u5957\u9910\u4ea7\u54c1\u8303\u56f4\u6216\u4ea7\u54c1\u4ee3\u7801\u6620\u5c04\u3002");
|
||||
TEMPLATES.put(Integer.valueOf(2), "\u3010\u9500\u552e\u4ef7\u672a\u5339\u914d\u56fd\u5bb6\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u5ba2\u6237 {customer}\uff0c\u4ea7\u54c1 {product}\uff0c\u76ee\u7684\u56fd\u5bb6 {country}\uff0c\u90ae\u7f16 {postcode}\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u5957\u9910\u300c{priceSet}\u300d\u7248\u672c {version}\u5df2\u547d\u4e2d\u4ea7\u54c1\uff0c\u4f46\u672a\u547d\u4e2d\u56fd\u5bb6/\u5206\u533a\uff0c\u8bf7\u8865\u9f50\u56fd\u5bb6\u4e0e\u5206\u533a\u4ef7\u683c\u3002");
|
||||
TEMPLATES.put(Integer.valueOf(3), "\u3010\u9500\u552e\u4ef7\u672a\u5339\u914d\u91cd\u91cf\u6bb5\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u5ba2\u6237 {customer}\uff0c\u4ea7\u54c1 {product}\uff0c\u56fd\u5bb6 {country}\uff0c\u5206\u533a {zone}\uff0c\u8ba1\u8d39\u91cd {chargeWeight} kg\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u5957\u9910\u300c{priceSet}\u300d\u7248\u672c {version}\u5df2\u547d\u4e2d\u4ea7\u54c1\u4e0e\u56fd\u5bb6\uff0c\u4f46\u8ba1\u8d39\u91cd\u672a\u843d\u5728\u4ef7\u683c\u91cd\u91cf\u6bb5\u5185\uff0c\u8bf7\u68c0\u67e5\u91cd\u91cf\u6bb5\u8bbe\u7f6e\u3002");
|
||||
TEMPLATES.put(Integer.valueOf(23), "\u3010\u4ea7\u54c1\u4ef7\u672a\u5339\u914d\u4ea7\u54c1\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u5ba2\u6237 {customer}\uff0c\u4ea7\u54c1 {product}\uff0c\u56fd\u5bb6 {country}\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u4ea7\u54c1\u4ef7\u76ee\u5f55\u4e2d\u672a\u627e\u5230\u8be5\u4ea7\u54c1\uff0c\u8bf7\u68c0\u67e5\u4ea7\u54c1\u4ef7\u6863\u6848\u662f\u5426\u5df2\u7ef4\u62a4\u3002");
|
||||
TEMPLATES.put(Integer.valueOf(24), "\u3010\u4ea7\u54c1\u4ef7\u672a\u5339\u914d\u56fd\u5bb6\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u4ea7\u54c1 {product}\uff0c\u76ee\u7684\u56fd\u5bb6 {country}\uff0c\u90ae\u7f16 {postcode}\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u4ea7\u54c1\u4ef7\u5df2\u547d\u4e2d\u4ea7\u54c1\uff0c\u4f46\u672a\u547d\u4e2d\u56fd\u5bb6/\u5206\u533a\uff0c\u8bf7\u8865\u9f50\u4ea7\u54c1\u4ef7\u56fd\u5bb6\u4ef7\u683c\u3002");
|
||||
TEMPLATES.put(Integer.valueOf(25), "\u3010\u4ea7\u54c1\u4ef7\u672a\u5339\u914d\u91cd\u91cf\u6bb5\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u4ea7\u54c1 {product}\uff0c\u56fd\u5bb6 {country}\uff0c\u5206\u533a {zone}\uff0c\u8ba1\u8d39\u91cd {chargeWeight} kg\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u4ea7\u54c1\u4ef7\u5df2\u547d\u4e2d\u4ea7\u54c1\u4e0e\u56fd\u5bb6\uff0c\u4f46\u8ba1\u8d39\u91cd\u672a\u843d\u5728\u91cd\u91cf\u6bb5\u5185\uff0c\u8bf7\u68c0\u67e5\u4ea7\u54c1\u4ef7\u91cd\u91cf\u6bb5\u3002");
|
||||
TEMPLATES.put(Integer.valueOf(4), "\u3010\u672a\u7ed1\u5b9a\u5957\u9910\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u5ba2\u6237 {customer}\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u8be5\u5ba2\u6237\u672a\u7ed1\u5b9a\u9500\u552e\u4ef7\u5957\u9910\uff0c\u65e0\u6cd5\u6309\u5957\u9910\u53d6\u4ef7\uff0c\u8bf7\u5148\u5728\u5ba2\u6237/\u5957\u9910\u7ed1\u5b9a\u540e\u518d\u8ba1\u8d39\u3002");
|
||||
TEMPLATES.put(Integer.valueOf(5), "\u3010\u5957\u9910\u672a\u542f\u7528/\u65e0\u751f\u6548\u7248\u672c\u3011\n\u8ba2\u5355 {orderNo}\uff08\u8f6c\u5355\u53f7 {transferNo}\uff09\uff0c\u5ba2\u6237 {customer}\uff0c\u5957\u9910\u300c{priceSet}\u300d\uff0c\u8ba1\u8d39\u65f6\u95f4 {bizDate}\u3002\n\u8ba1\u8d39\u65e5\u6ca1\u6709\u53ef\u7528\u7684\u5957\u9910\u751f\u6548\u7248\u672c\uff0c\u8bf7\u68c0\u67e5\u5957\u9910\u542f\u7528\u72b6\u6001\u4e0e\u7248\u672c\u751f\u6548\u671f\u3002");
|
||||
}
|
||||
|
||||
public static String buildDetail(OrderBillFailReasonVo vo, String fallback)
|
||||
{
|
||||
if (vo == null)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
Integer status = vo.getBillingFailStatus();
|
||||
String tpl = status == null ? null : TEMPLATES.get(status);
|
||||
if (StringUtils.isEmpty(tpl))
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\u3010").append(nz(vo.getBillingFailStatusLabel(), "\u8ba1\u8d39\u5931\u8d25")).append("\u3011\n");
|
||||
sb.append("\u8ba2\u5355 ").append(nz(vo.getOrderNo(), "-"));
|
||||
sb.append("\uff08\u8f6c\u5355\u53f7 ").append(nz(vo.getTransferNo(), "-")).append("\uff09");
|
||||
sb.append("\uff0c\u5ba2\u6237 ").append(joinCodeName(vo.getCustomerCode(), vo.getCustomerName()));
|
||||
sb.append("\uff0c\u4ea7\u54c1 ").append(joinCodeName(vo.getProductCode(), vo.getProductName()));
|
||||
sb.append("\uff0c\u56fd\u5bb6 ").append(joinCodeName(vo.getCountryCode(), vo.getCountryName()));
|
||||
sb.append("\uff0c\u8ba1\u8d39\u65f6\u95f4 ").append(fmtDate(vo.getBizDate())).append("\u3002\n");
|
||||
sb.append(nz(fallback, nz(vo.getErrorMessage(), vo.getBillingFailStatusLabel())));
|
||||
return sb.toString();
|
||||
}
|
||||
Map<String, String> ctx = new HashMap<String, String>();
|
||||
ctx.put("orderNo", nz(vo.getOrderNo(), "-"));
|
||||
ctx.put("transferNo", nz(vo.getTransferNo(), "-"));
|
||||
ctx.put("trackingNo", nz(vo.getTrackingNo(), "-"));
|
||||
ctx.put("customer", joinCodeName(vo.getCustomerCode(), vo.getCustomerName()));
|
||||
ctx.put("product", joinCodeName(vo.getProductCode(), vo.getProductName()));
|
||||
ctx.put("country", joinCodeName(vo.getCountryCode(), vo.getCountryName()));
|
||||
ctx.put("zone", joinCodeName(vo.getZoneCode(), vo.getZoneName()));
|
||||
ctx.put("postcode", nz(vo.getDestPostcode(), "-"));
|
||||
ctx.put("priceSet", joinCodeName(vo.getPriceSetCode(), vo.getPriceSetName()));
|
||||
ctx.put("version", nz(vo.getVersionNo(), "-"));
|
||||
ctx.put("chargeWeight", fmtNum(vo.getChargeWeight()));
|
||||
ctx.put("bizDate", fmtDate(vo.getBizDate()));
|
||||
ctx.put("pickupDate", fmtDate(vo.getPickupDate()));
|
||||
String out = tpl;
|
||||
for (Map.Entry<String, String> e : ctx.entrySet())
|
||||
{
|
||||
out = out.replace("{" + e.getKey() + "}", e.getValue());
|
||||
}
|
||||
if (StringUtils.isNotEmpty(fallback) && !fallback.equals(vo.getBillingFailStatusLabel()))
|
||||
{
|
||||
out = out + "\n\u8be6\u60c5\uff1a" + fallback;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String joinCodeName(String code, String name)
|
||||
{
|
||||
if (StringUtils.isEmpty(code) && StringUtils.isEmpty(name))
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
if (StringUtils.isEmpty(code))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
if (StringUtils.isEmpty(name) || name.equals(code))
|
||||
{
|
||||
return code;
|
||||
}
|
||||
return code + "/" + name;
|
||||
}
|
||||
|
||||
private static String fmtDate(Date d)
|
||||
{
|
||||
if (d == null)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(d);
|
||||
}
|
||||
|
||||
private static String fmtNum(BigDecimal n)
|
||||
{
|
||||
return n == null ? "-" : n.stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
private static String nz(String v, String def)
|
||||
{
|
||||
return StringUtils.isEmpty(v) ? def : v;
|
||||
}
|
||||
}
|
||||
+85
-8
@@ -28,11 +28,14 @@ import com.bms.price.config.BillingProperties;
|
||||
import com.bms.price.domain.BatchAddIncomeBillFeeTask;
|
||||
import com.bms.price.domain.PrcOrderBill;
|
||||
import com.bms.price.domain.vo.LocalRebillRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteResult;
|
||||
import com.bms.price.enums.BillBillingFailStatus;
|
||||
import com.bms.price.mapper.PrcOrderBillMapper;
|
||||
import com.bms.price.search.EsBizLogSupport;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.search.TabOrderSyncEsLog;
|
||||
import com.bms.price.service.IPrcOrderBillService;
|
||||
import com.bms.price.service.support.OrderBillExportSupport;
|
||||
|
||||
/**
|
||||
* Async local rebill from prc_order_bill only (no Oracle / MQ).
|
||||
@@ -122,16 +125,33 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
String one = ids.get(i);
|
||||
try
|
||||
{
|
||||
orderBillService.rebillLocalByBillId(one);
|
||||
success.incrementAndGet();
|
||||
TmsPriceQuoteResult result = orderBillService.rebillLocalByBillId(one);
|
||||
// Soft-fail still persists the order bill; count as fail in progress.
|
||||
if (BillBillingFailStatus.isFail(result == null ? null : result.getBillingFailStatus()))
|
||||
{
|
||||
fail.incrementAndGet();
|
||||
String tip = softFailMessage(result);
|
||||
String transferLabel = failTransferLabel(one,
|
||||
result == null ? null : result.getTransferNo());
|
||||
appendFail(failTips, transferLabel, tip);
|
||||
collectFailDetail(failDetails, one, tip);
|
||||
log.warn("local rebill soft-fail taskId={} billId={} transferNo={} status={} msg={}",
|
||||
taskId, one, transferLabel,
|
||||
result == null ? null : result.getBillingFailStatus(), tip);
|
||||
}
|
||||
else
|
||||
{
|
||||
success.incrementAndGet();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
fail.incrementAndGet();
|
||||
appendFail(failTips, one, e);
|
||||
String transferLabel = failTransferLabel(one, null);
|
||||
appendFail(failTips, transferLabel, e);
|
||||
collectFailDetail(failDetails, one, e);
|
||||
log.warn("local rebill failed taskId={} billId={} msg={}",
|
||||
taskId, one, e.getMessage());
|
||||
log.warn("local rebill failed taskId={} billId={} transferNo={} msg={}",
|
||||
taskId, one, transferLabel, e.getMessage());
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -250,6 +270,11 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
}
|
||||
|
||||
private void collectFailDetail(List<Map<String, Object>> failDetails, String billId, Exception e)
|
||||
{
|
||||
collectFailDetail(failDetails, billId, errorMessage(e));
|
||||
}
|
||||
|
||||
private void collectFailDetail(List<Map<String, Object>> failDetails, String billId, String message)
|
||||
{
|
||||
if (failDetails == null)
|
||||
{
|
||||
@@ -284,7 +309,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
item.put("orderId", StringUtils.isEmpty(orderNo) ? billId : orderNo);
|
||||
item.put("transferOrderId", transferNo);
|
||||
item.put("trackingNo", trackingNo);
|
||||
item.put("errorMessage", errorMessage(e));
|
||||
item.put("errorMessage", StringUtils.isEmpty(message) ? "fail" : message);
|
||||
failDetails.add(item);
|
||||
}
|
||||
}
|
||||
@@ -427,7 +452,12 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
return v;
|
||||
}
|
||||
|
||||
private static void appendFail(StringBuilder failTips, String billId, Exception e)
|
||||
private static void appendFail(StringBuilder failTips, String orderLabel, Exception e)
|
||||
{
|
||||
appendFail(failTips, orderLabel, errorMessage(e));
|
||||
}
|
||||
|
||||
private static void appendFail(StringBuilder failTips, String orderLabel, String message)
|
||||
{
|
||||
synchronized (failTips)
|
||||
{
|
||||
@@ -439,10 +469,57 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
{
|
||||
failTips.append("; ");
|
||||
}
|
||||
failTips.append(billId).append(":").append(errorMessage(e));
|
||||
failTips.append(StringUtils.isEmpty(orderLabel) ? "-" : orderLabel)
|
||||
.append(":").append(StringUtils.isEmpty(message) ? "fail" : message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer transferNo in UI tips; fall back to orderNo, then billId. */
|
||||
private String failTransferLabel(String billId, String transferNoHint)
|
||||
{
|
||||
if (StringUtils.isNotEmpty(transferNoHint))
|
||||
{
|
||||
return transferNoHint.trim();
|
||||
}
|
||||
if (StringUtils.isEmpty(billId) || orderBillMapper == null)
|
||||
{
|
||||
return billId;
|
||||
}
|
||||
try
|
||||
{
|
||||
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(billId.trim());
|
||||
if (bill != null)
|
||||
{
|
||||
if (StringUtils.isNotEmpty(bill.getTransferNo()))
|
||||
{
|
||||
return bill.getTransferNo().trim();
|
||||
}
|
||||
if (StringUtils.isNotEmpty(bill.getOrderNo()))
|
||||
{
|
||||
return bill.getOrderNo().trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ignore)
|
||||
{
|
||||
// tip label is best-effort
|
||||
}
|
||||
return billId;
|
||||
}
|
||||
|
||||
private static String softFailMessage(TmsPriceQuoteResult result)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
return "billing fail";
|
||||
}
|
||||
if (StringUtils.isNotEmpty(result.getMatchMessage()))
|
||||
{
|
||||
return result.getMatchMessage();
|
||||
}
|
||||
return OrderBillExportSupport.billingFailStatusLabel(result.getBillingFailStatus());
|
||||
}
|
||||
|
||||
private static String errorMessage(Exception e)
|
||||
{
|
||||
return StringUtils.isEmpty(e.getMessage()) ? e.getClass().getSimpleName() : e.getMessage();
|
||||
|
||||
@@ -36,6 +36,10 @@ public interface ArBillMapper {
|
||||
|
||||
int insertArBill(ArBill bill);
|
||||
|
||||
int updateProfitSnapshot(@Param("arBillId") Long arBillId,
|
||||
@Param("shenzhenArea") String shenzhenArea,
|
||||
@Param("guangzhouArea") String guangzhouArea);
|
||||
|
||||
int updateArBillStatus(@Param("arBillId") Long arBillId, @Param("status") String status,
|
||||
@Param("remark") String remark, @Param("operator") String operator,
|
||||
@Param("fromStatus") String fromStatus);
|
||||
|
||||
+6
@@ -935,6 +935,12 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
throw new ServiceException("账单生成结果为空或订单数量发生变化,请重新生成");
|
||||
}
|
||||
ArBill finalBill = billMapper.selectArBillById(task.getArBillId());
|
||||
// 明细与费用已全部落库;快照和 READY 状态必须在同一事务中提交。
|
||||
if (billMapper.updateProfitSnapshot(task.getArBillId(),
|
||||
com.bms.price.enums.ProfitAreaRule.SHENZHEN.getCustomerArea(),
|
||||
com.bms.price.enums.ProfitAreaRule.GUANGZHOU.getCustomerArea()) != 1) {
|
||||
throw new ServiceException("账单毛利快照更新失败,请重新生成");
|
||||
}
|
||||
billMapper.updateArBillGenerationStatus(task.getArBillId(),
|
||||
ArBillGenerationStatus.READY, task.getCreateBy(), null);
|
||||
if (generateTaskMapper.markSuccess(taskId, new Date()) != 1) {
|
||||
|
||||
@@ -6,37 +6,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="selectByChannelAndType" resultType="com.bms.price.domain.ChannelProductMap">
|
||||
select id, channel_id as channelId, channel_code as channelCode, product_id as productId,
|
||||
price_set_id as priceSetId, pay_type as payType, create_by as createBy, create_time as createTime,
|
||||
pay_type as payType, create_by as createBy, create_time as createTime,
|
||||
update_by as updateBy, update_time as updateTime
|
||||
from srv_channel_product_map
|
||||
where channel_id = #{channelId} and pay_type = #{payType}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectByPriceSetId" resultType="com.bms.price.domain.ChannelProductMap">
|
||||
select m.id, m.channel_id as channelId, m.channel_code as channelCode, m.product_id as productId,
|
||||
m.price_set_id as priceSetId, m.pay_type as payType,
|
||||
c.channel_name as channelName, p.product_name as productName,
|
||||
m.create_by as createBy, m.create_time as createTime,
|
||||
m.update_by as updateBy, m.update_time as updateTime
|
||||
from srv_channel_product_map m
|
||||
left join srv_service_channel c on c.id = m.channel_id
|
||||
left join pay_product p on p.product_id = m.product_id
|
||||
where m.price_set_id = #{priceSetId}
|
||||
and m.pay_type = 'AR'
|
||||
order by m.update_time desc, m.id desc
|
||||
</select>
|
||||
|
||||
<insert id="upsert">
|
||||
insert into srv_channel_product_map (
|
||||
channel_id, channel_code, product_id, price_set_id, pay_type, create_by, create_time, update_by, update_time
|
||||
channel_id, channel_code, product_id, pay_type, create_by, create_time, update_by, update_time
|
||||
) values (
|
||||
#{channelId}, #{channelCode}, #{productId}, #{priceSetId}, #{payType}, #{createBy}, sysdate(), #{updateBy}, sysdate()
|
||||
#{channelId}, #{channelCode}, #{productId}, #{payType}, #{createBy}, sysdate(), #{updateBy}, sysdate()
|
||||
)
|
||||
on duplicate key update
|
||||
channel_code = values(channel_code),
|
||||
product_id = values(product_id),
|
||||
price_set_id = values(price_set_id),
|
||||
update_by = values(update_by),
|
||||
update_time = sysdate()
|
||||
</insert>
|
||||
@@ -46,13 +32,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where channel_id = #{channelId} and pay_type = #{payType}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByChannelTypeAndPriceSet">
|
||||
delete from srv_channel_product_map
|
||||
where channel_id = #{channelId}
|
||||
and pay_type = #{payType}
|
||||
and price_set_id = #{priceSetId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByProductId">
|
||||
delete from srv_channel_product_map where product_id = #{productId}
|
||||
</delete>
|
||||
|
||||
@@ -41,15 +41,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectLabelPriceSetIdByProduct" resultType="long">
|
||||
select price_set_id
|
||||
from pay_product_term
|
||||
where product_id = #{productId}
|
||||
and price_set_id is not null
|
||||
order by case when status = '1' then 0 when status = '0' then 1 else 2 end, version_no desc, term_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!-- 打单套餐产品列表:待生效+生效 -->
|
||||
<select id="selectByPriceSetId" resultType="com.bms.price.domain.PayProductTerm">
|
||||
select t.term_id as termId, t.dict_value as dictValue, t.product_id as productId, t.price_set_id as priceSetId,
|
||||
|
||||
@@ -193,6 +193,49 @@
|
||||
#{confirmedBy}, #{confirmedTime}, #{createBy}, #{createUserId}, sysdate(), #{updateBy}, sysdate())
|
||||
</insert>
|
||||
|
||||
<!-- 只在生成完成时汇总当前账单;历史回填使用相同的校验口径。 -->
|
||||
<update id="updateProfitSnapshot">
|
||||
update ar_bill b
|
||||
left join (
|
||||
select d.ar_bill_id, count(*) as detail_count, sum(d.total_amount) as detail_total,
|
||||
count(distinct d.currency) as detail_currency_count, min(d.currency) as detail_currency,
|
||||
sum(case when d.currency is not null and f.currency_count = 1 and f.fee_currency = d.currency
|
||||
then 0 else 1 end) as currency_errors,
|
||||
sum(case when d.total_amount is not null and f.sales_amount is not null
|
||||
and f.sales_amount = d.total_amount then 0 else 1 end) as amount_errors,
|
||||
case when count(f.product_amount) = count(*) then sum(f.product_amount) else null end as product_total,
|
||||
case when count(f.redline_amount) = count(*) then sum(f.redline_amount) else null end as redline_total
|
||||
from ar_bill_detail d
|
||||
left join (
|
||||
select fee.detail_id,
|
||||
case when count(fee.amount) = count(*) then sum(fee.amount) else null end as sales_amount,
|
||||
case when count(fee.currency) = count(*) then count(distinct fee.currency) else 0 end as currency_count,
|
||||
min(fee.currency) as fee_currency,
|
||||
case when count(fee.product_amount) = count(*) then sum(fee.product_amount) else null end as product_amount,
|
||||
case when count(fee.redline_amount) = count(*) then sum(fee.redline_amount) else null end as redline_amount
|
||||
from ar_bill_detail_fee fee
|
||||
join ar_bill_detail fd on fd.detail_id = fee.detail_id
|
||||
where fd.ar_bill_id = #{arBillId}
|
||||
group by fee.detail_id
|
||||
) f on f.detail_id = d.detail_id
|
||||
where d.ar_bill_id = #{arBillId}
|
||||
group by d.ar_bill_id
|
||||
) a on a.ar_bill_id = b.ar_bill_id
|
||||
set b.product_amount = case when abs(a.product_total) <= 99999999999999.9999 then a.product_total else null end,
|
||||
b.redline_amount = case when abs(a.redline_total) <= 99999999999999.9999 then a.redline_total else null end,
|
||||
b.profit_calc_status = case
|
||||
when a.detail_count is null or a.detail_count = 0 then 'NO_DETAIL'
|
||||
when b.profit_customer_area is null or b.profit_customer_area not in (#{shenzhenArea}, #{guangzhouArea}) then 'MISSING_AREA'
|
||||
when b.currency is null or a.currency_errors > 0 or a.detail_currency_count != 1 or a.detail_currency != b.currency then 'CURRENCY_MISMATCH'
|
||||
when b.total_amount is null or a.amount_errors > 0 or a.detail_total != b.total_amount then 'AMOUNT_MISMATCH'
|
||||
when (b.profit_customer_area = #{shenzhenArea} and a.product_total is null)
|
||||
or (b.profit_customer_area = #{guangzhouArea} and a.redline_total is null) then 'MISSING_PRICE'
|
||||
when (b.profit_customer_area = #{shenzhenArea} and abs(a.product_total) > 99999999999999.9999)
|
||||
or (b.profit_customer_area = #{guangzhouArea} and abs(a.redline_total) > 99999999999999.9999) then 'PRICE_OVERFLOW'
|
||||
else 'READY' end
|
||||
where b.ar_bill_id = #{arBillId} and b.generation_status = 'GENERATING'
|
||||
</update>
|
||||
|
||||
<update id="updateArBillStatus">
|
||||
update ar_bill
|
||||
set status = #{status},
|
||||
|
||||
@@ -60,7 +60,16 @@
|
||||
<if test="query.endBillDate != null">and b.bill_date <= #{query.endBillDate}</if>
|
||||
</sql>
|
||||
|
||||
<!-- 按费用快照先聚合到订单,再聚合到完整账单,避免账单金额及费用重复。 -->
|
||||
<!-- 报表直接读取账单快照。 -->
|
||||
<sql id="validProfitSnapshot">
|
||||
b.profit_calc_status = 'READY' and b.total_amount is not null
|
||||
and ((b.profit_customer_area = #{shenzhenArea} and b.product_amount is not null)
|
||||
or (b.profit_customer_area = #{guangzhouArea} and b.redline_amount is not null))
|
||||
</sql>
|
||||
<sql id="areaProductAmount">
|
||||
case when b.profit_customer_area = #{shenzhenArea} then b.product_amount
|
||||
when b.profit_customer_area = #{guangzhouArea} then b.redline_amount else null end
|
||||
</sql>
|
||||
<sql id="billRows">
|
||||
<bind name="shenzhenArea" value="@com.bms.price.enums.ProfitAreaRule@SHENZHEN.getCustomerArea()"/>
|
||||
<bind name="guangzhouArea" value="@com.bms.price.enums.ProfitAreaRule@GUANGZHOU.getCustomerArea()"/>
|
||||
@@ -68,45 +77,12 @@
|
||||
b.profit_customer_area as customer_area,
|
||||
b.profit_sales_user_name as sales_user_name, b.currency, b.status,
|
||||
b.total_amount as sales_amount, b.written_off_amount, b.unwritten_amount,
|
||||
case when a.detail_count > 0 and a.invalid_count = 0 and b.total_amount is not null
|
||||
and a.detail_total = b.total_amount then a.product_total else null end as product_amount,
|
||||
case when a.detail_count > 0 and a.invalid_count = 0 and b.total_amount is not null
|
||||
and a.detail_total = b.total_amount then b.total_amount - a.product_total else null end as sales_gross_profit,
|
||||
case when a.detail_count > 0 and a.invalid_count = 0 and b.total_amount is not null
|
||||
and a.detail_total = b.total_amount and b.total_amount != 0
|
||||
then round((b.total_amount - a.product_total) * 100 / b.total_amount, 4) else null end as sales_gross_margin_rate,
|
||||
case when a.detail_count > 0 and a.invalid_count = 0 and b.total_amount is not null
|
||||
and a.detail_total = b.total_amount then 0 else 1 end as profit_incomplete
|
||||
case when <include refid="validProfitSnapshot"/> then <include refid="areaProductAmount"/> else null end as product_amount,
|
||||
case when <include refid="validProfitSnapshot"/> then b.total_amount - (<include refid="areaProductAmount"/>) else null end as sales_gross_profit,
|
||||
case when <include refid="validProfitSnapshot"/> and b.total_amount != 0
|
||||
then round((b.total_amount - (<include refid="areaProductAmount"/>)) * 100 / b.total_amount, 4) else null end as sales_gross_margin_rate,
|
||||
case when <include refid="validProfitSnapshot"/> then 0 else 1 end as profit_incomplete
|
||||
from ar_bill b
|
||||
left join (
|
||||
select d.ar_bill_id, count(*) as detail_count, sum(d.total_amount) as detail_total,
|
||||
sum(case when d.total_amount is not null and d.currency is not null and b.currency is not null
|
||||
and d.currency = b.currency and f.currency_count = 1 and f.fee_currency = d.currency
|
||||
and f.sales_amount is not null and f.sales_amount = d.total_amount
|
||||
and ((b.profit_customer_area = #{shenzhenArea} and f.product_complete = 1)
|
||||
or (b.profit_customer_area = #{guangzhouArea} and f.redline_complete = 1))
|
||||
then 0 else 1 end) as invalid_count,
|
||||
sum(case when b.profit_customer_area = #{shenzhenArea} then f.product_amount
|
||||
when b.profit_customer_area = #{guangzhouArea} then f.redline_amount else null end) as product_total
|
||||
from ar_bill_detail d join ar_bill b on b.ar_bill_id = d.ar_bill_id
|
||||
left join (
|
||||
select fee.detail_id,
|
||||
case when count(fee.amount) = count(*) then sum(fee.amount) else null end as sales_amount,
|
||||
case when count(fee.currency) = count(*) then count(distinct fee.currency) else 0 end as currency_count,
|
||||
min(fee.currency) as fee_currency,
|
||||
case when count(fee.product_amount) = count(*) then sum(fee.product_amount) else null end as product_amount,
|
||||
case when count(fee.redline_amount) = count(*) then sum(fee.redline_amount) else null end as redline_amount,
|
||||
case when count(fee.product_amount) = count(*) then 1 else 0 end as product_complete,
|
||||
case when count(fee.redline_amount) = count(*) then 1 else 0 end as redline_complete
|
||||
from ar_bill_detail_fee fee
|
||||
join ar_bill_detail d on d.detail_id = fee.detail_id
|
||||
join ar_bill b on b.ar_bill_id = d.ar_bill_id
|
||||
<include refid="billConditions"/>
|
||||
group by fee.detail_id
|
||||
) f on f.detail_id = d.detail_id
|
||||
<include refid="billConditions"/>
|
||||
group by d.ar_bill_id
|
||||
) a on a.ar_bill_id = b.ar_bill_id
|
||||
<include refid="billConditions"/>
|
||||
</sql>
|
||||
<select id="selectBills" resultMap="row">
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
-- 应收账单毛利汇总快照升级 v1,MySQL 5.7+。
|
||||
-- 前置:执行 20261105_ar_profit_report.sql;停止所有旧版后端(包括异步任务),在独立连接执行本文件,
|
||||
-- 核对末尾统计后再部署新版本。执行工具须支持 DELIMITER 和存储过程。
|
||||
-- DDL 自动提交,历史回填每批独立提交;异常修复后可重跑完整文件,已成功的快照不会覆盖。
|
||||
-- 历史客户区域沿用账单已有快照;NULL 表示未计算,异常账单不填零。
|
||||
-- READY 有效;NO_DETAIL 无明细;MISSING_AREA 区域缺失或不支持;MISSING_PRICE 缺价格;
|
||||
-- CURRENCY_MISMATCH 币种异常;AMOUNT_MISMATCH 金额不一致;PRICE_OVERFLOW 超出字段范围。
|
||||
SET NAMES utf8mb4;
|
||||
DROP PROCEDURE IF EXISTS bms_ar_bill_profit_snapshot_20261111_v1;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE bms_ar_bill_profit_snapshot_20261111_v1()
|
||||
BEGIN
|
||||
DECLARE n BIGINT DEFAULT 0;
|
||||
DECLARE first_id BIGINT DEFAULT NULL;
|
||||
DECLARE last_id BIGINT DEFAULT 0;
|
||||
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
RESIGNAL;
|
||||
END;
|
||||
|
||||
SELECT COUNT(*) INTO n FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name IN ('ar_bill', 'ar_bill_detail', 'ar_bill_detail_fee');
|
||||
IF n <> 3 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '毛利快照升级前置表缺失'; END IF;
|
||||
SELECT COUNT(*) INTO n FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND ((table_name = 'ar_bill' AND column_name IN ('ar_bill_id','profit_customer_area','total_amount','currency','generation_status'))
|
||||
OR (table_name = 'ar_bill_detail' AND column_name IN ('detail_id','ar_bill_id','total_amount','currency'))
|
||||
OR (table_name = 'ar_bill_detail_fee' AND column_name IN ('detail_id','amount','currency','product_amount','redline_amount')));
|
||||
IF n <> 14 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '请先完成应收毛利及费用价格快照升级'; END IF;
|
||||
SELECT COUNT(*) INTO n FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ar_bill'
|
||||
AND column_name IN ('product_amount', 'redline_amount', 'profit_calc_status')
|
||||
AND NOT (is_nullable = 'YES' AND ((column_name IN ('product_amount', 'redline_amount')
|
||||
AND data_type = 'decimal' AND numeric_precision = 18 AND numeric_scale = 4)
|
||||
OR (column_name = 'profit_calc_status' AND data_type = 'varchar' AND character_maximum_length = 24)));
|
||||
IF n > 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '已有账单毛利快照字段结构冲突'; END IF;
|
||||
|
||||
SELECT COUNT(*) INTO n FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ar_bill' AND column_name = 'product_amount';
|
||||
IF n = 0 THEN ALTER TABLE ar_bill ADD COLUMN product_amount DECIMAL(18,4) NULL COMMENT '账单产品价合计快照' AFTER total_amount; END IF;
|
||||
SELECT COUNT(*) INTO n FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ar_bill' AND column_name = 'redline_amount';
|
||||
IF n = 0 THEN ALTER TABLE ar_bill ADD COLUMN redline_amount DECIMAL(18,4) NULL COMMENT '账单红线价合计快照' AFTER product_amount; END IF;
|
||||
SELECT COUNT(*) INTO n FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ar_bill' AND column_name = 'profit_calc_status';
|
||||
IF n = 0 THEN ALTER TABLE ar_bill ADD COLUMN profit_calc_status VARCHAR(24) NULL COMMENT '毛利快照校验状态,NULL表示未计算' AFTER redline_amount; END IF;
|
||||
SELECT COUNT(*) INTO n FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ar_bill' AND is_nullable = 'YES'
|
||||
AND ((column_name IN ('product_amount', 'redline_amount') AND data_type = 'decimal' AND numeric_precision = 18 AND numeric_scale = 4)
|
||||
OR (column_name = 'profit_calc_status' AND data_type = 'varchar' AND character_maximum_length = 24));
|
||||
IF n <> 3 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '账单毛利快照字段结构冲突,请核对后重试'; END IF;
|
||||
SELECT COUNT(*) INTO n FROM ar_bill WHERE profit_calc_status IS NULL
|
||||
AND (product_amount IS NOT NULL OR redline_amount IS NOT NULL);
|
||||
IF n > 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '存在无状态的已填价格快照,请人工核对'; END IF;
|
||||
SELECT COUNT(*) INTO n FROM ar_bill WHERE profit_calc_status IS NOT NULL
|
||||
AND profit_calc_status NOT IN ('READY','NO_DETAIL','MISSING_AREA','CURRENCY_MISMATCH','AMOUNT_MISMATCH','MISSING_PRICE','PRICE_OVERFLOW');
|
||||
IF n > 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '存在未知毛利快照状态,请人工核对'; END IF;
|
||||
|
||||
backfill: LOOP
|
||||
SELECT MIN(ar_bill_id) INTO first_id FROM ar_bill
|
||||
WHERE generation_status = 'READY' AND (profit_calc_status IS NULL OR profit_calc_status != 'READY') AND ar_bill_id > last_id;
|
||||
IF first_id IS NULL THEN LEAVE backfill; END IF;
|
||||
SELECT MAX(batch.ar_bill_id) INTO last_id FROM (
|
||||
SELECT ar_bill_id FROM ar_bill WHERE generation_status = 'READY' AND (profit_calc_status IS NULL OR profit_calc_status != 'READY')
|
||||
AND ar_bill_id >= first_id ORDER BY ar_bill_id LIMIT 200
|
||||
) batch;
|
||||
START TRANSACTION;
|
||||
-- 与 ArBillMapper.updateProfitSnapshot 的汇总和校验保持一致。
|
||||
UPDATE ar_bill b
|
||||
LEFT JOIN (
|
||||
SELECT d.ar_bill_id, COUNT(*) AS detail_count, SUM(d.total_amount) AS detail_total,
|
||||
COUNT(DISTINCT d.currency) AS detail_currency_count, MIN(d.currency) AS detail_currency,
|
||||
SUM(CASE WHEN d.currency IS NOT NULL AND f.currency_count = 1 AND f.fee_currency = d.currency
|
||||
THEN 0 ELSE 1 END) AS currency_errors,
|
||||
SUM(CASE WHEN d.total_amount IS NOT NULL AND f.sales_amount IS NOT NULL
|
||||
AND f.sales_amount = d.total_amount THEN 0 ELSE 1 END) AS amount_errors,
|
||||
CASE WHEN COUNT(f.product_amount) = COUNT(*) THEN SUM(f.product_amount) ELSE NULL END AS product_total,
|
||||
CASE WHEN COUNT(f.redline_amount) = COUNT(*) THEN SUM(f.redline_amount) ELSE NULL END AS redline_total
|
||||
FROM ar_bill_detail d
|
||||
LEFT JOIN (
|
||||
SELECT fee.detail_id,
|
||||
CASE WHEN COUNT(fee.amount) = COUNT(*) THEN SUM(fee.amount) ELSE NULL END AS sales_amount,
|
||||
CASE WHEN COUNT(fee.currency) = COUNT(*) THEN COUNT(DISTINCT fee.currency) ELSE 0 END AS currency_count,
|
||||
MIN(fee.currency) AS fee_currency,
|
||||
CASE WHEN COUNT(fee.product_amount) = COUNT(*) THEN SUM(fee.product_amount) ELSE NULL END AS product_amount,
|
||||
CASE WHEN COUNT(fee.redline_amount) = COUNT(*) THEN SUM(fee.redline_amount) ELSE NULL END AS redline_amount
|
||||
FROM ar_bill_detail_fee fee
|
||||
JOIN ar_bill_detail fd ON fd.detail_id = fee.detail_id
|
||||
WHERE fd.ar_bill_id BETWEEN first_id AND last_id
|
||||
GROUP BY fee.detail_id
|
||||
) f ON f.detail_id = d.detail_id
|
||||
WHERE d.ar_bill_id BETWEEN first_id AND last_id
|
||||
GROUP BY d.ar_bill_id
|
||||
) a ON a.ar_bill_id = b.ar_bill_id
|
||||
SET b.product_amount = CASE WHEN ABS(a.product_total) <= 99999999999999.9999 THEN a.product_total ELSE NULL END,
|
||||
b.redline_amount = CASE WHEN ABS(a.redline_total) <= 99999999999999.9999 THEN a.redline_total ELSE NULL END,
|
||||
b.profit_calc_status = CASE
|
||||
WHEN a.detail_count IS NULL OR a.detail_count = 0 THEN 'NO_DETAIL'
|
||||
WHEN b.profit_customer_area IS NULL OR b.profit_customer_area NOT IN ('C', 'GZ') THEN 'MISSING_AREA'
|
||||
WHEN b.currency IS NULL OR a.currency_errors > 0 OR a.detail_currency_count != 1 OR a.detail_currency != b.currency THEN 'CURRENCY_MISMATCH'
|
||||
WHEN b.total_amount IS NULL OR a.amount_errors > 0 OR a.detail_total != b.total_amount THEN 'AMOUNT_MISMATCH'
|
||||
WHEN (b.profit_customer_area = 'C' AND a.product_total IS NULL)
|
||||
OR (b.profit_customer_area = 'GZ' AND a.redline_total IS NULL) THEN 'MISSING_PRICE'
|
||||
WHEN (b.profit_customer_area = 'C' AND ABS(a.product_total) > 99999999999999.9999)
|
||||
OR (b.profit_customer_area = 'GZ' AND ABS(a.redline_total) > 99999999999999.9999) THEN 'PRICE_OVERFLOW'
|
||||
ELSE 'READY' END
|
||||
WHERE b.ar_bill_id BETWEEN first_id AND last_id AND b.generation_status = 'READY'
|
||||
AND (b.profit_calc_status IS NULL OR b.profit_calc_status != 'READY');
|
||||
COMMIT;
|
||||
END LOOP;
|
||||
SELECT COALESCE(profit_calc_status, 'NOT_CALCULATED') AS profit_calc_status, COUNT(*) AS bill_count
|
||||
FROM ar_bill WHERE generation_status = 'READY' GROUP BY profit_calc_status ORDER BY profit_calc_status;
|
||||
SELECT COUNT(*) AS ready_unprocessed FROM ar_bill WHERE generation_status = 'READY' AND profit_calc_status IS NULL;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL bms_ar_bill_profit_snapshot_20261111_v1();
|
||||
DROP PROCEDURE IF EXISTS bms_ar_bill_profit_snapshot_20261111_v1;
|
||||
@@ -76,6 +76,9 @@ CREATE TABLE `ar_bill` (
|
||||
`currency` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'CNY' COMMENT '币种',
|
||||
`order_count` int(11) NOT NULL DEFAULT '0' COMMENT '订单数量',
|
||||
`total_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '账单应收总额',
|
||||
`product_amount` decimal(18,4) DEFAULT NULL COMMENT '账单产品价合计快照',
|
||||
`redline_amount` decimal(18,4) DEFAULT NULL COMMENT '账单红线价合计快照',
|
||||
`profit_calc_status` varchar(24) DEFAULT NULL COMMENT '毛利快照校验状态,NULL表示未计算',
|
||||
`written_off_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '已核销金额缓存',
|
||||
`unwritten_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '未核销金额缓存',
|
||||
`status` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '状态:DRAFT=草稿,CONFIRMED=已确认,PARTIAL_WRITTEN_OFF=部分核销,WRITTEN_OFF=已核销,VOID=已作废',
|
||||
|
||||
@@ -6,7 +6,6 @@ CREATE TABLE IF NOT EXISTS `srv_channel_product_map` (
|
||||
`channel_id` varchar(50) NOT NULL COMMENT '渠道ID,对应 srv_service_channel.id',
|
||||
`channel_code` varchar(64) DEFAULT NULL COMMENT '渠道编码,对应 srv_service_channel.channel_code',
|
||||
`product_id` bigint(20) NOT NULL COMMENT '产品ID,应付/应收时为价卡产品编号',
|
||||
`price_set_id` bigint(20) DEFAULT NULL COMMENT '应收套餐ID,对应 prc_price_set.price_set_id',
|
||||
`pay_type` varchar(10) NOT NULL COMMENT '收付类型,字典 bms_pay_type:AR应收 AP应付',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
@@ -15,23 +14,9 @@ CREATE TABLE IF NOT EXISTS `srv_channel_product_map` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_channel_pay_type` (`channel_id`, `pay_type`),
|
||||
KEY `idx_channel_product_map_product` (`product_id`),
|
||||
KEY `idx_channel_product_map_price_set` (`price_set_id`),
|
||||
KEY `idx_channel_product_map_channel_code` (`channel_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道产品关系映射';
|
||||
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE srv_channel_product_map ADD COLUMN price_set_id bigint(20) DEFAULT NULL COMMENT ''应收套餐ID,对应 prc_price_set.price_set_id'' AFTER product_id, ADD KEY idx_channel_product_map_price_set (price_set_id)',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'srv_channel_product_map' AND COLUMN_NAME = 'price_set_id'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql_channel_code = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
|
||||
@@ -65,6 +65,15 @@ export function listOrderBill(query) {
|
||||
})
|
||||
}
|
||||
|
||||
/** Order-list: billing fail reason popup */
|
||||
export function getOrderBillFailReason(billId) {
|
||||
return request({
|
||||
url: '/price/billing/orderBill/' + billId + '/failReason',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/** Order-bill product check: package sales products missing from catalog */
|
||||
export function checkOrderBillProducts(data) {
|
||||
return request({
|
||||
|
||||
@@ -162,24 +162,3 @@ export function removeLabelProducts(priceSetId, productIds) {
|
||||
})
|
||||
}
|
||||
|
||||
export function listLabelChannels(priceSetId) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/channels',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function bindLabelChannels(priceSetId, data) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/channels',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function unbindLabelChannel(priceSetId, channelId) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/channels/' + channelId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,9 +106,6 @@
|
||||
<template #col-chargeWeight="{ row }">
|
||||
{{ formatWeight(row.chargeWeight) }}
|
||||
</template>
|
||||
<template #col-matchSource="{ row }">
|
||||
<el-tag size="small" :type="sourceTagType(row.matchSource)">{{ sourceLabel(row.matchSource) }}</el-tag>
|
||||
</template>
|
||||
<template #col-orderStatus="{ row }">
|
||||
{{ orderStatusLabel(row.orderStatus) }}
|
||||
</template>
|
||||
@@ -565,7 +562,6 @@ const columns = [
|
||||
{ label: '收货重', prop: 'receiveWeight', align: 'center', width: 90, slot: 'chargeWeight' },
|
||||
{ label: '应收总额', prop: 'incomeTotal', align: 'right', width: 120, slot: 'incomeTotal' },
|
||||
{ label: '应付总额', prop: 'payTotal', align: 'right', width: 120, slot: 'payTotal' },
|
||||
{ label: '计价来源', prop: 'matchSource', align: 'center', width: 130, slot: 'matchSource' },
|
||||
{ label: '订单状态', prop: 'orderStatus', align: 'center', width: 110, slot: 'orderStatus' },
|
||||
{ label: '计费状态', prop: 'billingFailStatus', align: 'center', width: 120, slot: 'billingFailStatus' },
|
||||
{ label: '客户状态', prop: 'customerActiveStatus', align: 'center', width: 100, slot: 'customerActiveStatus' },
|
||||
@@ -643,19 +639,6 @@ const searchColumns = [
|
||||
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '\u8ba1\u8d39\u5931\u8d25',
|
||||
prop: 'billingFailStatuses',
|
||||
component: 'select',
|
||||
options: billingFailStatusOptions,
|
||||
placeholder: '\u8bf7\u9009\u62e9',
|
||||
props: {
|
||||
multiple: true,
|
||||
clearable: true,
|
||||
collapseTags: true,
|
||||
collapseTagsTooltip: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
function buildBillQuery(params) {
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
<template>
|
||||
<set-section-card
|
||||
:title="title"
|
||||
tone="blue"
|
||||
:count="rows.length"
|
||||
:sub="sub"
|
||||
>
|
||||
<template #action>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="Plus"
|
||||
:disabled="!priceSetId"
|
||||
@click="openBind"
|
||||
v-hasPermi="['price:label:edit']"
|
||||
>添加渠道</el-button>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" border empty-text="暂未绑定渠道">
|
||||
<el-table-column label="渠道编码" prop="channelCode" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="渠道名称" prop="channelName" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="应收产品" prop="productName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" @click="unbind(row)" v-hasPermi="['price:label:edit']">解绑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="bindOpen" title="绑定渠道" width="560px" append-to-body @closed="onClosed">
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="套餐产品" required>
|
||||
<el-select v-model="form.productId" filterable clearable placeholder="选择本套餐内应收产品" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in productOptions"
|
||||
:key="item.productId"
|
||||
:label="item.productName"
|
||||
:value="item.productId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="渠道" required>
|
||||
<el-select
|
||||
v-model="form.channelIds"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="可多选渠道"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
:key="item.id"
|
||||
:label="channelLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="hint">绑定后会写入渠道应收产品,并关联本打单套餐。</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="bindOpen = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</set-section-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SetSectionCard from '../../set/components/detail/SetSectionCard.vue'
|
||||
import { listLabelChannels, bindLabelChannels, unbindLabelChannel, listLabelProducts } from '@/api/price/label'
|
||||
import { listChannelOptions } from '@/api/masterdata/serviceChannel'
|
||||
|
||||
const props = defineProps({
|
||||
priceSetId: { type: [Number, String], default: null }
|
||||
})
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const title = '绑定渠道'
|
||||
const sub = '渠道计费时按应收产品 + 本套餐取价'
|
||||
const loading = ref(false)
|
||||
const rows = ref([])
|
||||
const bindOpen = ref(false)
|
||||
const saving = ref(false)
|
||||
const productOptions = ref([])
|
||||
const channelOptions = ref([])
|
||||
const form = reactive({
|
||||
productId: undefined,
|
||||
channelIds: []
|
||||
})
|
||||
|
||||
function channelLabel(item) {
|
||||
const code = item.channelCode || item.id || ''
|
||||
const name = item.channelName || ''
|
||||
return name ? (code + ' / ' + name) : String(code)
|
||||
}
|
||||
|
||||
async function loadRows() {
|
||||
if (!props.priceSetId) {
|
||||
rows.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listLabelChannels(props.priceSetId)
|
||||
rows.value = res.data || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openBind() {
|
||||
if (!props.priceSetId) return
|
||||
form.productId = undefined
|
||||
form.channelIds = []
|
||||
const [prodRes, chRes] = await Promise.all([
|
||||
listLabelProducts(props.priceSetId),
|
||||
listChannelOptions({ blOpen: '1' })
|
||||
])
|
||||
productOptions.value = prodRes.data || []
|
||||
channelOptions.value = chRes.data || []
|
||||
if (!productOptions.value.length) {
|
||||
proxy.$modal.msgWarning('请先导入套餐产品价卡')
|
||||
return
|
||||
}
|
||||
bindOpen.value = true
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
form.productId = undefined
|
||||
form.channelIds = []
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!form.productId) {
|
||||
proxy.$modal.msgWarning('请选择套餐内应收产品')
|
||||
return
|
||||
}
|
||||
if (!form.channelIds.length) {
|
||||
proxy.$modal.msgWarning('请选择渠道')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
bindLabelChannels(props.priceSetId, {
|
||||
productId: form.productId,
|
||||
channelIds: form.channelIds
|
||||
}).then(res => {
|
||||
proxy.$modal.msgSuccess(res.msg || res.data || '绑定成功')
|
||||
bindOpen.value = false
|
||||
loadRows()
|
||||
}).finally(() => {
|
||||
saving.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function unbind(row) {
|
||||
if (!row?.channelId) return
|
||||
const name = row.channelName || row.channelCode || row.channelId
|
||||
proxy.$modal.confirm('确认解绑渠道「' + name + '」?').then(() => {
|
||||
return unbindLabelChannel(props.priceSetId, row.channelId)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('已解绑')
|
||||
loadRows()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
watch(() => props.priceSetId, () => loadRows(), { immediate: true })
|
||||
|
||||
defineExpose({ reload: loadRows })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hint {
|
||||
margin: -4px 0 0 110px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -14,8 +14,6 @@
|
||||
|
||||
<set-customer-table :set="setInfo" @refresh="loadHeader" />
|
||||
|
||||
<label-channel-table ref="channelTableRef" :price-set-id="priceSetId" />
|
||||
|
||||
<label-version-table
|
||||
:versions="versions"
|
||||
@edit="openEditVersion"
|
||||
@@ -83,7 +81,6 @@ import SetCustomerTable from '../set/components/detail/SetCustomerTable.vue'
|
||||
import LabelImportDialog from './components/LabelImportDialog.vue'
|
||||
import LabelVersionTable from './components/LabelVersionTable.vue'
|
||||
import LabelVersionProductDrawer from './components/LabelVersionProductDrawer.vue'
|
||||
import LabelChannelTable from './components/LabelChannelTable.vue'
|
||||
|
||||
const labelModuleApi = {
|
||||
...priceSetApi,
|
||||
@@ -133,7 +130,6 @@ const setInfo = reactive({
|
||||
})
|
||||
const versions = ref([])
|
||||
const importRef = ref()
|
||||
const channelTableRef = ref()
|
||||
const productDrawerVisible = ref(false)
|
||||
const activeVersion = ref(null)
|
||||
|
||||
@@ -183,7 +179,6 @@ async function loadVersions() {
|
||||
async function reloadAll() {
|
||||
await loadHeader()
|
||||
await loadVersions()
|
||||
channelTableRef.value?.reload?.()
|
||||
}
|
||||
|
||||
function openVersionDetails(row) {
|
||||
|
||||
@@ -186,13 +186,19 @@
|
||||
{{ orderStatusLabel(row.orderStatus) }}
|
||||
</template>
|
||||
<template #col-billingFailStatus="{ row }">
|
||||
<el-tag size="small" :type="billingFailTagType(row.billingFailStatus)">{{ billingFailLabel(row.billingFailStatus) }}</el-tag>
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="billingFailTagType(row.billingFailStatus)"
|
||||
class="billing-fail-tag"
|
||||
:class="{ 'is-clickable': Number(row.billingFailStatus) !== 0 }"
|
||||
@click="openFailReason(row)"
|
||||
>{{ billingFailLabel(row.billingFailStatus) }}</el-tag>
|
||||
</template>
|
||||
</basic-table>
|
||||
<el-dialog
|
||||
v-model="rebillProgressVisible"
|
||||
:title="rebillDialogTitle"
|
||||
width="480px"
|
||||
width="560px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="true"
|
||||
@@ -206,11 +212,33 @@
|
||||
:striped-flow="batchLoading"
|
||||
/>
|
||||
<div class="rebill-progress__msg">{{ rebillProgressMessage }}</div>
|
||||
<div class="rebill-progress__text">
|
||||
<div v-if="!batchLoading && rebillFinished" class="rebill-result-stats">
|
||||
<div class="rebill-result-stats__item">
|
||||
<span class="rebill-result-stats__label">总数</span>
|
||||
<strong>{{ rebillProgressTotal }}</strong>
|
||||
</div>
|
||||
<div class="rebill-result-stats__item is-success">
|
||||
<span class="rebill-result-stats__label">成功</span>
|
||||
<strong>{{ rebillProgressSuccess }}</strong>
|
||||
</div>
|
||||
<div class="rebill-result-stats__item is-fail">
|
||||
<span class="rebill-result-stats__label">失败</span>
|
||||
<strong>{{ rebillProgressFail }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="rebill-progress__text">
|
||||
总数 {{ rebillProgressTotal }}
|
||||
/ 成功 {{ rebillProgressSuccess }}
|
||||
/ 失败 {{ rebillProgressFail }}
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="!batchLoading && rebillFailDetail"
|
||||
:title="rebillFailDetail"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="rebill-progress__error"
|
||||
/>
|
||||
<p v-if="batchLoading" class="rebill-progress__hint">计费在后台进行,可收起弹窗继续操作;请勿关闭或刷新本页</p>
|
||||
<template #footer>
|
||||
<el-button v-if="batchLoading" @click="rebillProgressVisible = false">收起</el-button>
|
||||
@@ -221,7 +249,21 @@
|
||||
:loading="rebillCancelLoading"
|
||||
@click="handleCancelRebill"
|
||||
>取消计费</el-button>
|
||||
<el-button v-else type="primary" @click="rebillProgressVisible = false">关闭</el-button>
|
||||
<template v-else>
|
||||
<el-button
|
||||
v-if="rebillProgressFail > 0"
|
||||
type="warning"
|
||||
plain
|
||||
@click="viewRebillFailedOrders"
|
||||
>查看失败订单</el-button>
|
||||
<el-button
|
||||
v-if="rebillProgressFail > 0"
|
||||
type="warning"
|
||||
:loading="exportLoading"
|
||||
@click="exportRebillFailedOrders"
|
||||
>导出失败订单</el-button>
|
||||
<el-button type="primary" @click="rebillProgressVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
@@ -258,12 +300,92 @@
|
||||
:fields="ORDER_BILL_EXPORT_FIELDS"
|
||||
storage-key="exportFields:orderBill"
|
||||
/>
|
||||
<el-dialog
|
||||
v-model="failReasonVisible"
|
||||
title="计费失败详情"
|
||||
width="780px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="fail-reason-dialog"
|
||||
>
|
||||
<div v-loading="failReasonLoading" class="fail-reason-body">
|
||||
<div v-if="failReason.failReasonDetail || failReason.errorMessage" class="fail-reason-banner">
|
||||
<div class="fail-reason-banner__badge">{{ failReason.billingFailStatusLabel || billingFailLabel(failReason.billingFailStatus) }}</div>
|
||||
<div class="fail-reason-text">{{ failReason.failReasonDetail || failReason.errorMessage }}</div>
|
||||
</div>
|
||||
<el-descriptions :column="2" border size="small" class="fail-reason-desc">
|
||||
<el-descriptions-item label="订单号">
|
||||
<span class="fr-val">{{ failReason.orderNo || '-' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="转单号">
|
||||
<span class="fr-val">{{ failReason.transferNo || '-' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="运单号">
|
||||
<span class="fr-val">{{ failReason.trackingNo || '-' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="计费状态">
|
||||
<el-tag size="small" type="danger" effect="dark">{{ failReason.billingFailStatusLabel || billingFailLabel(failReason.billingFailStatus) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label-class-name="fr-label-key" class-name="fr-cell-key" label="客户">
|
||||
<span :class="frValueClass(joinCodeName(failReason.customerCode, failReason.customerName), true)">{{ joinCodeName(failReason.customerCode, failReason.customerName) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label-class-name="fr-label-key" class-name="fr-cell-key" label="产品">
|
||||
<span :class="frValueClass(joinCodeName(failReason.productCode, failReason.productName), true)">{{ joinCodeName(failReason.productCode, failReason.productName) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label-class-name="fr-label-key" class-name="fr-cell-key" label="国家">
|
||||
<span :class="frValueClass(joinCodeName(failReason.countryCode, failReason.countryName), true)">{{ joinCodeName(failReason.countryCode, failReason.countryName) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="分区">
|
||||
<span :class="frValueClass(joinCodeName(failReason.zoneCode, failReason.zoneName))">{{ joinCodeName(failReason.zoneCode, failReason.zoneName) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收件人邮编">
|
||||
<span :class="frValueClass(failReason.destPostcode || '-')">{{ failReason.destPostcode || '-' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label-class-name="fr-label-key" class-name="fr-cell-key" label="套餐">
|
||||
<span :class="frValueClass(joinCodeName(failReason.priceSetCode, failReason.priceSetName), true)">{{ joinCodeName(failReason.priceSetCode, failReason.priceSetName) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="套餐版本">
|
||||
<span :class="frValueClass(failReason.versionNo || '-')">{{ failReason.versionNo || '-' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="重量段">
|
||||
<span :class="frValueClass(failReason.weightLabel || formatWeightRange(failReason.weightMin, failReason.weightMax))">{{ failReason.weightLabel || formatWeightRange(failReason.weightMin, failReason.weightMax) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label-class-name="fr-label-key" class-name="fr-cell-key" label="计费重">
|
||||
<span :class="frValueClass(formatWeight(failReason.chargeWeight), true)">{{ formatWeight(failReason.chargeWeight) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="原计费重">
|
||||
<span class="fr-val">{{ formatWeight(failReason.weight) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="单价">
|
||||
<span class="fr-val fr-val-money">{{ formatMoney(failReason.freightRate, failReason.currency) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="运费金额">
|
||||
<span class="fr-val fr-val-money">{{ formatMoney(failReason.freightAmount, failReason.currency) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="应收总额">
|
||||
<span class="fr-val fr-val-money">{{ formatMoney(failReason.incomeTotal, failReason.currency) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="计费时间">
|
||||
<span class="fr-val">{{ formatDateTime(failReason.bizDate) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label-class-name="fr-label-key" class-name="fr-cell-key" label="收货时间">
|
||||
<span :class="frValueClass(formatDateTime(failReason.pickupDate), true)">{{ formatDateTime(failReason.pickupDate) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="原始失败信息" :span="2">
|
||||
<span class="fr-val fr-val-raw">{{ failReason.errorMessage || '-' }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="failReasonVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="OrderBillQuery">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listOrderBill, localRebillAsync, pollLocalRebillTask, cancelLocalRebill, getLocalRebillTask } from '@/api/price/billing'
|
||||
import { listOrderBill, localRebillAsync, pollLocalRebillTask, cancelLocalRebill, getLocalRebillTask, getOrderBillFailReason } from '@/api/price/billing'
|
||||
import { exportAsyncFromTable } from '@/utils/excelExport'
|
||||
import ExcelExportFieldDialog from '@/components/ExcelExportFieldDialog/index.vue'
|
||||
import { ORDER_BILL_EXPORT_FIELDS } from '@/utils/exportFieldDefs'
|
||||
@@ -356,6 +478,7 @@ const rebillProgressStatus = ref(undefined)
|
||||
const rebillProgressTotal = ref(0)
|
||||
const rebillProgressSuccess = ref(0)
|
||||
const rebillProgressFail = ref(0)
|
||||
const rebillFailDetail = ref('')
|
||||
const rebillTaskId = ref('')
|
||||
const rebillCancelLoading = ref(false)
|
||||
let rebillFloatHideTimer = null
|
||||
@@ -429,6 +552,7 @@ const defaultParams = {
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ label: '计费状态', prop: 'billingFailStatus', align: 'center', width: 160, fixed: 'left', slot: 'billingFailStatus' },
|
||||
{ label: '订单号', prop: 'orderNo', width: 160, fixed: 'left', showOverflowTooltip: true },
|
||||
{ label: '运单号', prop: 'trackingNo', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '转单号', prop: 'transferNo', minWidth: 140, showOverflowTooltip: true },
|
||||
@@ -442,7 +566,6 @@ const columns = [
|
||||
{ label: '实重', prop: 'receiveWeight', align: 'center', width: 90, slot: 'receiveWeight' },
|
||||
{ label: '收件人邮编', prop: 'destPostcode', minWidth: 120, showOverflowTooltip: true },
|
||||
{ label: '订单状态', prop: 'orderStatus', align: 'center', width: 110, slot: 'orderStatus' },
|
||||
{ label: '计费状态', prop: 'billingFailStatus', align: 'center', width: 140, slot: 'billingFailStatus' },
|
||||
{ label: '计费时间', prop: 'bizDate', align: 'center', width: 170, parseTime: true },
|
||||
// { label: '下单时间', prop: 'orderTime', align: 'center', width: 170, parseTime: true },
|
||||
{ label: '提货时间', prop: 'pickupDate', align: 'center', width: 170, parseTime: true },
|
||||
@@ -767,21 +890,52 @@ function applyRebillProgress(t) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function billingFailOnlyStatuses() {
|
||||
// All statuses except success(0).
|
||||
return billingFailStatusOptions.map(o => o.value).filter(v => v !== 0)
|
||||
}
|
||||
|
||||
function viewRebillFailedOrders() {
|
||||
rebillProgressVisible.value = false
|
||||
const fails = billingFailOnlyStatuses()
|
||||
tableRef.value?.setQueryParams?.({ billingFailStatuses: fails })
|
||||
tableRef.value?.handleQuery?.()
|
||||
}
|
||||
|
||||
async function exportRebillFailedOrders() {
|
||||
if (exportLoading.value) return
|
||||
const fails = billingFailOnlyStatuses()
|
||||
// Keep current list filters, force fail statuses for export.
|
||||
tableRef.value?.setQueryParams?.({ billingFailStatuses: fails })
|
||||
rebillProgressVisible.value = false
|
||||
await handleExport()
|
||||
}
|
||||
|
||||
function finishRebillFromTask(task, cancelled) {
|
||||
const success = Number(task && task.success) || 0
|
||||
const fail = Number(task && task.fail) || 0
|
||||
const total = Number(task && task.total) || rebillProgressTotal.value || 0
|
||||
rebillProgressPercent.value = Math.min(100, Number(task && task.percent) || 100)
|
||||
rebillProgressStatus.value = cancelled ? 'warning' : (fail > 0 ? 'warning' : 'success')
|
||||
rebillProgressTotal.value = Number(task && task.total) || rebillProgressTotal.value
|
||||
rebillProgressTotal.value = total
|
||||
rebillProgressSuccess.value = success
|
||||
rebillProgressFail.value = fail
|
||||
rebillProgressMessage.value = cancelled
|
||||
? ('已取消:成功 ' + success + ' / 失败 ' + fail)
|
||||
: ('完成:成功 ' + success + ' / 失败 ' + fail)
|
||||
rebillFailDetail.value = fail > 0 ? String((task && (task.error || task.message)) || '').trim() : ''
|
||||
if (cancelled) {
|
||||
rebillProgressMessage.value = '重新计费已取消:成功 ' + success + ' / 失败 ' + fail + ' / 总数 ' + total
|
||||
} else if (fail > 0) {
|
||||
rebillProgressMessage.value = '重新计费完成(含失败):成功 ' + success + ' / 失败 ' + fail + ' / 总数 ' + total
|
||||
} else {
|
||||
rebillProgressMessage.value = '重新计费完成:成功 ' + success + ' / 失败 ' + fail + ' / 总数 ' + total
|
||||
}
|
||||
rebillFinished.value = true
|
||||
clearRebillTaskId()
|
||||
tableRef.value?.refresh?.()
|
||||
// Always popup result dialog when billing finishes (even if progress was collapsed).
|
||||
rebillProgressVisible.value = true
|
||||
if (cancelled) ElMessage.warning(rebillProgressMessage.value)
|
||||
else if (fail > 0) ElMessage.warning(rebillProgressMessage.value)
|
||||
else ElMessage.success(rebillProgressMessage.value)
|
||||
scheduleHideRebillFloat()
|
||||
}
|
||||
@@ -799,6 +953,7 @@ async function resumeRebillTask(taskId) {
|
||||
rebillProgressTotal.value = 0
|
||||
rebillProgressSuccess.value = 0
|
||||
rebillProgressFail.value = 0
|
||||
rebillFailDetail.value = ''
|
||||
let superseded = false
|
||||
try {
|
||||
const result = await pollLocalRebillTask(taskId, applyRebillProgress)
|
||||
@@ -811,6 +966,7 @@ async function resumeRebillTask(taskId) {
|
||||
const task = e && e.task
|
||||
rebillProgressStatus.value = 'exception'
|
||||
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
|
||||
rebillFailDetail.value = String((task && task.error) || (e && e.message) || '').trim()
|
||||
rebillFinished.value = true
|
||||
if (task) {
|
||||
rebillProgressPercent.value = Math.min(100, Number(task.percent) || 100)
|
||||
@@ -875,6 +1031,7 @@ async function runLocalRebill(payload, confirmText) {
|
||||
rebillProgressTotal.value = 0
|
||||
rebillProgressSuccess.value = 0
|
||||
rebillProgressFail.value = 0
|
||||
rebillFailDetail.value = ''
|
||||
window.setTimeout(() => {
|
||||
if (batchLoading.value) rebillProgressVisible.value = false
|
||||
}, 600)
|
||||
@@ -894,6 +1051,7 @@ async function runLocalRebill(payload, confirmText) {
|
||||
const task = e && e.task
|
||||
rebillProgressStatus.value = 'exception'
|
||||
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
|
||||
rebillFailDetail.value = String((task && task.error) || (e && e.message) || '').trim()
|
||||
rebillFinished.value = true
|
||||
if (task) {
|
||||
rebillProgressPercent.value = Math.min(100, Number(task.percent) || 100)
|
||||
@@ -996,6 +1154,133 @@ function orderStatusLabel(status) {
|
||||
return String(status)
|
||||
}
|
||||
|
||||
|
||||
const failReasonVisible = ref(false)
|
||||
const failReasonLoading = ref(false)
|
||||
const failReason = reactive({
|
||||
billId: '',
|
||||
orderNo: '',
|
||||
transferNo: '',
|
||||
trackingNo: '',
|
||||
customerCode: '',
|
||||
customerName: '',
|
||||
productCode: '',
|
||||
productName: '',
|
||||
countryCode: '',
|
||||
countryName: '',
|
||||
zoneCode: '',
|
||||
zoneName: '',
|
||||
destPostcode: '',
|
||||
priceSetCode: '',
|
||||
priceSetName: '',
|
||||
versionNo: '',
|
||||
weightLabel: '',
|
||||
weightMin: null,
|
||||
weightMax: null,
|
||||
chargeWeight: null,
|
||||
weight: null,
|
||||
freightRate: null,
|
||||
freightAmount: null,
|
||||
incomeTotal: null,
|
||||
currency: '',
|
||||
bizDate: null,
|
||||
pickupDate: null,
|
||||
billingFailStatus: null,
|
||||
billingFailStatusLabel: '',
|
||||
errorMessage: '',
|
||||
failReasonDetail: ''
|
||||
})
|
||||
|
||||
function joinCodeName(code, name) {
|
||||
if (!code && !name) return '-'
|
||||
if (!code) return name
|
||||
if (!name || name === code) return code
|
||||
return code + ' / ' + name
|
||||
}
|
||||
|
||||
function frValueClass(text, keyField) {
|
||||
const empty = text == null || text === '' || text === '-'
|
||||
if (empty) return 'fr-val fr-val-empty'
|
||||
return keyField ? 'fr-val fr-val-key' : 'fr-val'
|
||||
}
|
||||
|
||||
function formatWeightRange(min, max) {
|
||||
if (min == null && max == null) return '-'
|
||||
return (min == null ? '-' : formatWeight(min)) + ' ~ ' + (max == null ? '-' : formatWeight(max))
|
||||
}
|
||||
|
||||
function formatMoney(val, currency) {
|
||||
if (val == null || val === '') return '-'
|
||||
const num = Number(val)
|
||||
if (Number.isNaN(num)) return String(val)
|
||||
return (currency ? (currency + ' ') : '') + num.toFixed(4)
|
||||
}
|
||||
|
||||
function formatDateTime(val) {
|
||||
if (!val) return '-'
|
||||
const d = new Date(val)
|
||||
if (Number.isNaN(d.getTime())) return String(val)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
|
||||
' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
|
||||
}
|
||||
|
||||
function applyFailReason(data, row) {
|
||||
const src = data || {}
|
||||
const base = row || {}
|
||||
failReason.billId = src.billId || base.billId || ''
|
||||
failReason.orderNo = src.orderNo || base.orderNo || ''
|
||||
failReason.transferNo = src.transferNo || base.transferNo || ''
|
||||
failReason.trackingNo = src.trackingNo || base.trackingNo || ''
|
||||
failReason.customerCode = src.customerCode || base.customerCode || ''
|
||||
failReason.customerName = src.customerName || base.customerName || ''
|
||||
failReason.productCode = src.productCode || base.productCode || ''
|
||||
failReason.productName = src.productName || base.productName || ''
|
||||
failReason.countryCode = src.countryCode || base.countryCode || ''
|
||||
failReason.countryName = src.countryName || base.countryName || ''
|
||||
failReason.zoneCode = src.zoneCode || ''
|
||||
failReason.zoneName = src.zoneName || ''
|
||||
failReason.destPostcode = src.destPostcode || base.destPostcode || ''
|
||||
failReason.priceSetCode = src.priceSetCode || base.priceSetCode || ''
|
||||
failReason.priceSetName = src.priceSetName || base.priceSetName || ''
|
||||
failReason.versionNo = src.versionNo || ''
|
||||
failReason.weightLabel = src.weightLabel || base.weightLabel || ''
|
||||
failReason.weightMin = src.weightMin != null ? src.weightMin : base.weightMin
|
||||
failReason.weightMax = src.weightMax != null ? src.weightMax : base.weightMax
|
||||
failReason.chargeWeight = src.chargeWeight != null ? src.chargeWeight : base.chargeWeight
|
||||
failReason.weight = src.weight != null ? src.weight : base.weight
|
||||
failReason.freightRate = src.freightRate != null ? src.freightRate : base.freightRate
|
||||
failReason.freightAmount = src.freightAmount != null ? src.freightAmount : base.freightAmount
|
||||
failReason.incomeTotal = src.incomeTotal != null ? src.incomeTotal : base.incomeTotal
|
||||
failReason.currency = src.currency || base.currency || ''
|
||||
failReason.bizDate = src.bizDate || base.bizDate || null
|
||||
failReason.pickupDate = src.pickupDate || base.pickupDate || null
|
||||
failReason.billingFailStatus = src.billingFailStatus != null ? src.billingFailStatus : base.billingFailStatus
|
||||
failReason.billingFailStatusLabel = src.billingFailStatusLabel || billingFailLabel(failReason.billingFailStatus)
|
||||
failReason.errorMessage = src.errorMessage || ''
|
||||
failReason.failReasonDetail = src.failReasonDetail || src.errorMessage || billingFailLabel(failReason.billingFailStatus)
|
||||
}
|
||||
|
||||
async function openFailReason(row) {
|
||||
if (!row || !row.billId) return
|
||||
const code = row.billingFailStatus == null ? 0 : Number(row.billingFailStatus)
|
||||
if (code === 0) return
|
||||
failReasonVisible.value = true
|
||||
failReasonLoading.value = true
|
||||
applyFailReason({}, row)
|
||||
failReason.errorMessage = ''
|
||||
failReason.failReasonDetail = ''
|
||||
try {
|
||||
const res = await getOrderBillFailReason(row.billId)
|
||||
applyFailReason((res && res.data) || {}, row)
|
||||
} catch (e) {
|
||||
failReason.errorMessage = (e && e.message) || '查询失败原因失败'
|
||||
failReason.failReasonDetail = failReason.errorMessage
|
||||
} finally {
|
||||
failReasonLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function billingFailLabel(status) {
|
||||
const code = status == null ? 0 : Number(status)
|
||||
const hit = billingFailStatusOptions.find(item => item.value === code)
|
||||
@@ -1049,6 +1334,39 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.order-identifier-search { width: 100%; }
|
||||
.rebill-progress__msg { margin-top: 12px; color: var(--el-text-color-regular); }
|
||||
.rebill-result-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.rebill-result-stats__item {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-light);
|
||||
text-align: center;
|
||||
}
|
||||
.rebill-result-stats__item.is-success {
|
||||
background: var(--el-color-success-light-9);
|
||||
}
|
||||
.rebill-result-stats__item.is-fail {
|
||||
background: var(--el-color-warning-light-9);
|
||||
}
|
||||
.rebill-result-stats__label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.rebill-result-stats__item strong {
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.rebill-progress__error {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.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); }
|
||||
.order-bill-query .export-status-float {
|
||||
@@ -1117,4 +1435,74 @@ onBeforeUnmount(() => {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.billing-fail-tag.is-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.fail-reason-body {
|
||||
min-height: 180px;
|
||||
}
|
||||
.fail-reason-banner {
|
||||
margin-bottom: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f3a5a5;
|
||||
background: linear-gradient(180deg, #fff5f5 0%, #ffecec 100%);
|
||||
}
|
||||
.fail-reason-banner__badge {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: #e11d48;
|
||||
}
|
||||
.fail-reason-text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.65;
|
||||
font-size: 13px;
|
||||
color: #9f1239;
|
||||
}
|
||||
.fail-reason-desc {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.fail-reason-desc :deep(.el-descriptions__label) {
|
||||
width: 110px;
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
font-weight: 500;
|
||||
}
|
||||
.fail-reason-desc :deep(.fr-label-key) {
|
||||
color: #9a3412;
|
||||
background: #fff7ed;
|
||||
font-weight: 700;
|
||||
}
|
||||
.fail-reason-desc :deep(.fr-cell-key) {
|
||||
background: #fffbeb;
|
||||
}
|
||||
.fr-val {
|
||||
color: #0f172a;
|
||||
font-weight: 500;
|
||||
}
|
||||
.fr-val-key {
|
||||
color: #c2410c;
|
||||
font-weight: 700;
|
||||
}
|
||||
.fr-val-empty {
|
||||
color: #dc2626;
|
||||
font-weight: 700;
|
||||
background: #fee2e2;
|
||||
padding: 1px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.fr-val-money {
|
||||
color: #b91c1c;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.fr-val-raw {
|
||||
color: #7f1d1d;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user