Merge branch 'test' into feauture-20261104
# Conflicts: # bms-front/src/views/price/billing/components/BillDetail.vue
This commit is contained in:
+31
-2
@@ -16,6 +16,7 @@ import com.bms.common.core.page.TableSupport;
|
||||
import com.bms.price.search.BillSearchPage;
|
||||
import com.bms.price.search.EsLogType;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.search.PriceChangeGroupPage;
|
||||
|
||||
/**
|
||||
* 来自 ES 的价格变更前后日志(销售价 / 产品价 / 红线价)。
|
||||
@@ -31,12 +32,40 @@ public class PriceLogController extends BaseController
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(
|
||||
@RequestParam(value = "bizId", required = false) String bizId,
|
||||
@RequestParam(value = "status", required = false) String status)
|
||||
@RequestParam(value = "status", required = false) String status,
|
||||
@RequestParam(value = "psvId", required = false) Long psvId,
|
||||
@RequestParam(value = "priceKind", required = false) String priceKind,
|
||||
@RequestParam(value = "productCode", required = false) String productCode,
|
||||
@RequestParam(value = "updateTime", required = false) String updateTime,
|
||||
@RequestParam(value = "updateTimePrefix", required = false) String updateTimePrefix,
|
||||
@RequestParam(value = "countryKeyword", required = false) String countryKeyword)
|
||||
{
|
||||
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
|
||||
int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue();
|
||||
BillSearchPage page = billSearchStore.search(EsLogType.PRICE_CHANGE, bizId, status, pageNum, pageSize);
|
||||
BillSearchPage page = billSearchStore.search(EsLogType.PRICE_CHANGE, bizId, status, psvId, priceKind,
|
||||
productCode, updateTime, updateTimePrefix, countryKeyword, pageNum, pageSize);
|
||||
TableDataInfo rsp = new TableDataInfo();
|
||||
rsp.setCode(HttpStatus.SUCCESS);
|
||||
rsp.setMsg("查询成功");
|
||||
rsp.setRows(page.getRows());
|
||||
rsp.setTotal(page.getTotal());
|
||||
return rsp;
|
||||
}
|
||||
|
||||
/** Group by productCode + write time for package-version drill-down. */
|
||||
@PreAuthorize("@ss.hasPermi('monitor:priceLog:list')")
|
||||
@GetMapping("/groups")
|
||||
public TableDataInfo groups(
|
||||
@RequestParam(value = "psvId", required = true) Long psvId,
|
||||
@RequestParam(value = "status", required = false) String status,
|
||||
@RequestParam(value = "priceKind", required = false) String priceKind)
|
||||
{
|
||||
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
|
||||
int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue();
|
||||
PriceChangeGroupPage page = billSearchStore.searchPriceChangeGroups(psvId, priceKind, status, pageNum,
|
||||
pageSize);
|
||||
TableDataInfo rsp = new TableDataInfo();
|
||||
rsp.setCode(HttpStatus.SUCCESS);
|
||||
rsp.setMsg("查询成功");
|
||||
|
||||
@@ -22,9 +22,12 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.bms.common.annotation.Log;
|
||||
import com.bms.common.constant.HttpStatus;
|
||||
import com.bms.common.core.controller.BaseController;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.page.PageDomain;
|
||||
import com.bms.common.core.page.TableDataInfo;
|
||||
import com.bms.common.core.page.TableSupport;
|
||||
import com.bms.common.enums.BusinessType;
|
||||
import com.bms.common.utils.DateUtils;
|
||||
import com.bms.common.utils.MessageUtils;
|
||||
@@ -54,6 +57,11 @@ import com.bms.price.domain.vo.TmsPriceQuoteRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteResult;
|
||||
import com.bms.price.enums.BillTaskSuccessStatus;
|
||||
import com.bms.price.enums.BillTaskType;
|
||||
import com.bms.price.search.BillSearchPage;
|
||||
import com.bms.price.search.BillSyncSnapshot;
|
||||
import com.bms.price.search.EsBizLogSupport;
|
||||
import com.bms.price.search.EsLogType;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.service.IPrcBillingTaskService;
|
||||
import com.bms.price.service.IPrcOrderBillService;
|
||||
import com.bms.price.service.ITmsPriceQuoteService;
|
||||
@@ -84,6 +92,9 @@ public class BillingController extends BaseController
|
||||
@Autowired
|
||||
private OrderBillProductCheckService orderBillProductCheckService;
|
||||
|
||||
@Autowired
|
||||
private IBillSearchStore billSearchStore;
|
||||
|
||||
/**
|
||||
* Historical quote list.
|
||||
*/
|
||||
@@ -355,6 +366,97 @@ public class BillingController extends BaseController
|
||||
return success(prcOrderBillService.getBill(billId));
|
||||
}
|
||||
|
||||
/**
|
||||
* ES BILL logs for this order (billing / upstream sync / fail snapshots).
|
||||
* bizId prefers transferNo then orderNo (same as write path).
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:bill') or @ss.hasPermi('price:orderBill:list')")
|
||||
@GetMapping("/bill/{billId}/history")
|
||||
public TableDataInfo billHistory(@PathVariable("billId") String billId)
|
||||
{
|
||||
if (StringUtils.isEmpty(billId))
|
||||
{
|
||||
return getDataTable(new ArrayList<BillSyncSnapshot>());
|
||||
}
|
||||
TmsPriceQuoteResult bill = prcOrderBillService.getBill(billId.trim());
|
||||
if (bill == null)
|
||||
{
|
||||
TableDataInfo empty = new TableDataInfo();
|
||||
empty.setCode(HttpStatus.SUCCESS);
|
||||
empty.setMsg("查询成功");
|
||||
empty.setRows(new ArrayList<BillSyncSnapshot>());
|
||||
empty.setTotal(0L);
|
||||
return empty;
|
||||
}
|
||||
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
|
||||
int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue();
|
||||
String primary = EsBizLogSupport.billBizId(bill.getTransferNo(), bill.getOrderNo());
|
||||
BillSearchPage page = billSearchStore.search(EsLogType.BILL, primary, null, pageNum, pageSize);
|
||||
// Also pull by orderNo when transferNo was used as bizId, so older orderNo-keyed rows appear.
|
||||
if (StringUtils.isNotEmpty(bill.getTransferNo()) && StringUtils.isNotEmpty(bill.getOrderNo())
|
||||
&& !bill.getTransferNo().trim().equalsIgnoreCase(bill.getOrderNo().trim())
|
||||
&& pageNum == 1)
|
||||
{
|
||||
BillSearchPage byOrder = billSearchStore.search(EsLogType.BILL, bill.getOrderNo().trim(), null, 1,
|
||||
Math.max(pageSize, 50));
|
||||
page = mergeBillHistory(page, byOrder, pageSize);
|
||||
}
|
||||
TableDataInfo rsp = new TableDataInfo();
|
||||
rsp.setCode(HttpStatus.SUCCESS);
|
||||
rsp.setMsg("查询成功");
|
||||
rsp.setRows(page.getRows());
|
||||
rsp.setTotal(page.getTotal());
|
||||
return rsp;
|
||||
}
|
||||
|
||||
private BillSearchPage mergeBillHistory(BillSearchPage primary, BillSearchPage secondary, int pageSize)
|
||||
{
|
||||
BillSearchPage out = new BillSearchPage();
|
||||
Map<String, BillSyncSnapshot> byId = new HashMap<String, BillSyncSnapshot>();
|
||||
if (primary != null && primary.getRows() != null)
|
||||
{
|
||||
for (BillSyncSnapshot row : primary.getRows())
|
||||
{
|
||||
if (row != null && StringUtils.isNotEmpty(row.getDocId()))
|
||||
{
|
||||
byId.put(row.getDocId(), row);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (secondary != null && secondary.getRows() != null)
|
||||
{
|
||||
for (BillSyncSnapshot row : secondary.getRows())
|
||||
{
|
||||
if (row != null && StringUtils.isNotEmpty(row.getDocId()) && !byId.containsKey(row.getDocId()))
|
||||
{
|
||||
byId.put(row.getDocId(), row);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<BillSyncSnapshot> merged = new ArrayList<BillSyncSnapshot>(byId.values());
|
||||
merged.sort((a, b) -> {
|
||||
String ta = a == null || a.getUpdateTime() == null ? "" : a.getUpdateTime();
|
||||
String tb = b == null || b.getUpdateTime() == null ? "" : b.getUpdateTime();
|
||||
return tb.compareTo(ta);
|
||||
});
|
||||
long total = Math.max(primary == null ? 0L : primary.getTotal(), merged.size());
|
||||
if (secondary != null)
|
||||
{
|
||||
total = Math.max(total, secondary.getTotal());
|
||||
}
|
||||
out.setTotal(total);
|
||||
if (merged.size() > pageSize)
|
||||
{
|
||||
out.setRows(new ArrayList<BillSyncSnapshot>(merged.subList(0, pageSize)));
|
||||
}
|
||||
else
|
||||
{
|
||||
out.setRows(merged);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refund one receivable bill via billing task billType=REFUND.
|
||||
*/
|
||||
|
||||
+39
@@ -1,18 +1,26 @@
|
||||
package com.bms.web.controller.price;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.bms.common.annotation.Log;
|
||||
import com.bms.common.core.controller.BaseController;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.page.TableDataInfo;
|
||||
import com.bms.common.enums.BusinessType;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
|
||||
import com.bms.price.airbill.domain.SrvBillAirFeeSave;
|
||||
import com.bms.price.domain.vo.SrvBillAirQuery;
|
||||
import com.bms.price.domain.vo.SrvBillAirVo;
|
||||
import com.bms.price.service.ISrvBillAirFeeService;
|
||||
import com.bms.price.service.ISrvBillAirService;
|
||||
|
||||
/**
|
||||
@@ -25,6 +33,9 @@ public class SrvBillAirController extends BaseController
|
||||
@Autowired
|
||||
private ISrvBillAirService srvBillAirService;
|
||||
|
||||
@Autowired
|
||||
private ISrvBillAirFeeService srvBillAirFeeService;
|
||||
|
||||
/**
|
||||
* 提单列表
|
||||
*/
|
||||
@@ -55,4 +66,32 @@ public class SrvBillAirController extends BaseController
|
||||
}
|
||||
return success(vo);
|
||||
}
|
||||
|
||||
/** 一票提单下未删除的费用 */
|
||||
@PreAuthorize("@ss.hasPermi('price:airBill:fee')")
|
||||
@GetMapping("/fee/list")
|
||||
public AjaxResult feeList(@RequestParam("billCode") String billCode)
|
||||
{
|
||||
return success(srvBillAirFeeService.listFees(billCode));
|
||||
}
|
||||
|
||||
/** 提单费用的新增、修改、删除日志,按页返回 */
|
||||
@PreAuthorize("@ss.hasPermi('price:airBill:fee')")
|
||||
@GetMapping("/fee/log")
|
||||
public TableDataInfo feeLog(@RequestParam("billCode") String billCode)
|
||||
{
|
||||
List<SrvBillAirFeeLog> list = srvBillAirFeeService.listLogs(billCode);
|
||||
TableDataInfo table = getDataTable(list);
|
||||
table.setRows(new ArrayList<>(list));
|
||||
return table;
|
||||
}
|
||||
|
||||
/** 只保存本次新增、修改、删除的费用,并写入提单日志 */
|
||||
@PreAuthorize("@ss.hasPermi('price:airBill:fee')")
|
||||
@Log(title = "提单费用", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/fee/save")
|
||||
public AjaxResult saveFee(@RequestBody SrvBillAirFeeSave command)
|
||||
{
|
||||
return success(srvBillAirFeeService.save(command));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ spring:
|
||||
initialSize: 5
|
||||
# 最小连接池数量
|
||||
minIdle: 10
|
||||
# 最大连接池数量
|
||||
maxActive: 20
|
||||
# 最大连接池数量(local rebill 16 workers + UI/MQ headroom)
|
||||
maxActive: 40
|
||||
# 配置获取连接等待超时的时间
|
||||
maxWait: 60000
|
||||
# 配置连接超时时间
|
||||
|
||||
@@ -51,8 +51,8 @@ spring:
|
||||
initialSize: 5
|
||||
# 最小连接池数量
|
||||
minIdle: 10
|
||||
# 最大连接池数量
|
||||
maxActive: 20
|
||||
# 最大连接池数量(local rebill 16 workers + UI/MQ headroom)
|
||||
maxActive: 40
|
||||
# 配置获取连接等待超时的时间
|
||||
maxWait: 60000
|
||||
# 配置连接超时时间
|
||||
|
||||
@@ -40,6 +40,8 @@ bms:
|
||||
# 同步已直接复用订单行(跳过每单 Oracle 公网查询),瓶颈在 MySQL 计费写入,
|
||||
# 同客户行锁冲突由 billWithDbRetry 3 次重试兜底,16 路实测吞吐最高。
|
||||
order-source-sync-threads: 16
|
||||
# Order-list local rebill workers (2~24). Pair with Druid maxActive >= threads + 8.
|
||||
local-rebill-threads: 16
|
||||
# Night auto-rebill: hours 0-8, pull 500 (all statuses) -> bill -> next 500 until empty / 09:00.
|
||||
# Empty page => no billing.
|
||||
night-retry:
|
||||
|
||||
+5
@@ -450,6 +450,11 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
|
||||
realChanges.add(c);
|
||||
}
|
||||
}
|
||||
// Rebind/update with no field diff: skip empty UPDATE noise (full package save upsert)
|
||||
if (existing != null && realChanges.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
saveLog(bind.getCustomerId(), customer == null ? null : customer.getCustomerCode(),
|
||||
customer == null ? null : customer.getCustomerName(),
|
||||
BmsCustomerLog.MODULE_PRICE_SET, newSet.getPriceSetName(),
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.bms.price.airbill.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 空运提单费用明细 srv_bill_air_fee。
|
||||
*/
|
||||
@Data
|
||||
public class SrvBillAirFee
|
||||
{
|
||||
private Long id;
|
||||
|
||||
private String billCode;
|
||||
|
||||
/** 字典 bms_fee_type */
|
||||
private String feeType;
|
||||
|
||||
private BigDecimal price;
|
||||
|
||||
/** 字典 bms_currency */
|
||||
private String currency;
|
||||
|
||||
private String remark;
|
||||
|
||||
private String createBy;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createTime;
|
||||
|
||||
private String updateBy;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updateTime;
|
||||
|
||||
/** 0未删除 1已删除 */
|
||||
private String delFlag;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.bms.price.airbill.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 空运提单费用操作日志。
|
||||
*/
|
||||
@Data
|
||||
public class SrvBillAirFeeLog
|
||||
{
|
||||
public static final String INSERT = "1";
|
||||
|
||||
public static final String UPDATE = "2";
|
||||
|
||||
public static final String DELETE = "3";
|
||||
|
||||
private Long logId;
|
||||
|
||||
private String billCode;
|
||||
|
||||
private Long feeId;
|
||||
|
||||
private String operationType;
|
||||
|
||||
private String summary;
|
||||
|
||||
private String changeContent;
|
||||
|
||||
private String createBy;
|
||||
|
||||
private String createByName;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createTime;
|
||||
|
||||
private List<FieldChange> changes;
|
||||
|
||||
@Data
|
||||
public static class FieldChange
|
||||
{
|
||||
private String field;
|
||||
|
||||
private String fieldLabel;
|
||||
|
||||
private String oldValue;
|
||||
|
||||
private String newValue;
|
||||
|
||||
public FieldChange()
|
||||
{
|
||||
}
|
||||
|
||||
public FieldChange(String field, String fieldLabel, String oldValue, String newValue)
|
||||
{
|
||||
this.field = field;
|
||||
this.fieldLabel = fieldLabel;
|
||||
this.oldValue = oldValue;
|
||||
this.newValue = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.bms.price.airbill.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 保存提单费用时只带有变动的行。未出现的费用保持原样。
|
||||
*/
|
||||
@Data
|
||||
public class SrvBillAirFeeSave
|
||||
{
|
||||
private String billCode;
|
||||
|
||||
private List<SrvBillAirFee> inserts = new ArrayList<>();
|
||||
|
||||
private List<SrvBillAirFee> updates = new ArrayList<>();
|
||||
|
||||
private List<Long> deletes = new ArrayList<>();
|
||||
}
|
||||
@@ -20,6 +20,11 @@ public class BillingProperties
|
||||
private int consumerThreads = 8;
|
||||
/** Concurrent threads for monitor order-source sync */
|
||||
private int orderSourceSyncThreads = 8;
|
||||
/**
|
||||
* Concurrent threads for order-list local rebill (2~24).
|
||||
* Keep below Druid maxActive with headroom for other traffic.
|
||||
*/
|
||||
private int localRebillThreads = 16;
|
||||
/** Night window auto-retry of prc_billing_task (all statuses) */
|
||||
private NightRetry nightRetry = new NightRetry();
|
||||
private Oracle oracle = new Oracle();
|
||||
@@ -84,6 +89,16 @@ public class BillingProperties
|
||||
this.orderSourceSyncThreads = orderSourceSyncThreads;
|
||||
}
|
||||
|
||||
public int getLocalRebillThreads()
|
||||
{
|
||||
return localRebillThreads;
|
||||
}
|
||||
|
||||
public void setLocalRebillThreads(int localRebillThreads)
|
||||
{
|
||||
this.localRebillThreads = localRebillThreads;
|
||||
}
|
||||
|
||||
public NightRetry getNightRetry()
|
||||
{
|
||||
return nightRetry;
|
||||
|
||||
+4
@@ -40,6 +40,10 @@ public class BatchAddIncomeBillFeeRequest implements Serializable
|
||||
private String beginBizDate;
|
||||
private String endBizDate;
|
||||
|
||||
/** Receive/pickup time range (pickup_date), inclusive. */
|
||||
private String beginPickupDate;
|
||||
private String endPickupDate;
|
||||
|
||||
/** Surcharge config id (prc_surcharge.sc_id). */
|
||||
@NotNull(message = "scId required")
|
||||
private Long scId;
|
||||
|
||||
+3
@@ -25,4 +25,7 @@ public class ChargeWeightTemplateRequest implements Serializable
|
||||
private List<Integer> orderStatuses;
|
||||
private String beginBizDate;
|
||||
private String endBizDate;
|
||||
/** Receive/pickup time range (pickup_date), inclusive. */
|
||||
private String beginPickupDate;
|
||||
private String endPickupDate;
|
||||
}
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ public class PrcBillingBillListExportRow implements Serializable
|
||||
@Excel(name = "计费时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date bizDate;
|
||||
@Excel(name = "提货时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "收货时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date pickupDate;
|
||||
@Excel(name = "销售员", width = 12)
|
||||
|
||||
@@ -31,4 +31,7 @@ public class RecalcBillPriceRequest implements Serializable
|
||||
private List<Integer> billingFailStatuses;
|
||||
private String beginBizDate;
|
||||
private String endBizDate;
|
||||
/** Receive/pickup time range (pickup_date), inclusive. */
|
||||
private String beginPickupDate;
|
||||
private String endPickupDate;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ public interface PrcPriceSetDetailMapper
|
||||
|
||||
public int countByPsvId(Long psvId);
|
||||
|
||||
/** Count detail rows including soft-deleted (for deleted-version tab). */
|
||||
public int countByPsvIdAll(Long psvId);
|
||||
|
||||
public int copyDetailsFromVersion(@Param("srcPsvId") Long srcPsvId, @Param("newPsvId") Long newPsvId);
|
||||
|
||||
/** Copy version effective time, expire time and status onto catalog detail rows. */
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.bms.price.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.price.airbill.domain.SrvBillAirFee;
|
||||
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
|
||||
|
||||
/**
|
||||
* 空运提单费用及操作日志。
|
||||
*/
|
||||
public interface SrvBillAirFeeMapper
|
||||
{
|
||||
List<SrvBillAirFee> selectByBillCode(String billCode);
|
||||
|
||||
/** 只取这一票提单上仍有效的费用,供修改和删除前核对。 */
|
||||
SrvBillAirFee selectLive(@Param("id") Long id, @Param("billCode") String billCode);
|
||||
|
||||
int insertFee(SrvBillAirFee fee);
|
||||
|
||||
int updateFee(SrvBillAirFee fee);
|
||||
|
||||
int deleteFee(@Param("id") Long id, @Param("billCode") String billCode, @Param("updateBy") String updateBy,
|
||||
@Param("updateTime") java.util.Date updateTime);
|
||||
|
||||
int insertLog(SrvBillAirFeeLog log);
|
||||
|
||||
List<SrvBillAirFeeLog> selectLogs(String billCode);
|
||||
}
|
||||
@@ -1232,15 +1232,11 @@ public class TabOrderMonitorService
|
||||
query.setOrderStatuses(cleaned.isEmpty() ? new ArrayList<Integer>(DEFAULT_ORDER_STATUSES) : cleaned);
|
||||
}
|
||||
|
||||
/** Empty or anything other than 0/1 falls back to 1(已收货). */
|
||||
/** Always force BL_PICKUP=1; UI no longer exposes this filter. */
|
||||
private static void normalizeBlPickup(TabOrderMonitorQuery query)
|
||||
{
|
||||
Integer blPickup = query.getBlPickup();
|
||||
if (blPickup == null || (blPickup.intValue() != 0 && blPickup.intValue() != 1))
|
||||
{
|
||||
query.setBlPickup(Integer.valueOf(1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多行文本归一化:按换行/逗号/分号/顿号拆分(不按空格拆),trim、去空去重。
|
||||
|
||||
@@ -3,7 +3,11 @@ package com.bms.price.search;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpEntity;
|
||||
@@ -148,6 +152,36 @@ public class EsBillSearchStore implements IBillSearchStore
|
||||
|
||||
@Override
|
||||
public BillSearchPage search(String logType, String bizId, String status, int pageNum, int pageSize)
|
||||
{
|
||||
return search(logType, bizId, status, null, null, pageNum, pageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
|
||||
int pageNum, int pageSize)
|
||||
{
|
||||
return search(logType, bizId, status, psvId, priceKind, null, null, pageNum, pageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
|
||||
String productCode, String updateTime, int pageNum, int pageSize)
|
||||
{
|
||||
return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, null, pageNum, pageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
|
||||
String productCode, String updateTime, String updateTimePrefix, int pageNum, int pageSize)
|
||||
{
|
||||
return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, updateTimePrefix,
|
||||
null, pageNum, pageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
|
||||
String productCode, String updateTime, String updateTimePrefix, String countryKeyword,
|
||||
int pageNum, int pageSize)
|
||||
{
|
||||
BillSearchPage page = new BillSearchPage();
|
||||
if (pageNum < 1)
|
||||
@@ -171,7 +205,8 @@ public class EsBillSearchStore implements IBillSearchStore
|
||||
sortItem.put("updatedAt.keyword", sortOpt);
|
||||
sort.add(sortItem);
|
||||
body.put("sort", sort);
|
||||
body.put("query", buildQuery(logType, bizId, status));
|
||||
body.put("query", buildQuery(logType, bizId, status, psvId, priceKind, productCode, updateTime,
|
||||
updateTimePrefix, countryKeyword));
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<String> entity = new HttpEntity<String>(body.toJSONString(), headers);
|
||||
@@ -217,6 +252,108 @@ public class EsBillSearchStore implements IBillSearchStore
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PriceChangeGroupPage searchPriceChangeGroups(Long psvId, String priceKind, String status,
|
||||
int pageNum, int pageSize)
|
||||
{
|
||||
PriceChangeGroupPage page = new PriceChangeGroupPage();
|
||||
if (psvId == null)
|
||||
{
|
||||
return page;
|
||||
}
|
||||
if (pageNum < 1)
|
||||
{
|
||||
pageNum = 1;
|
||||
}
|
||||
if (pageSize < 1)
|
||||
{
|
||||
pageSize = 10;
|
||||
}
|
||||
List<BillSyncSnapshot> all = loadAllPriceChanges(psvId, priceKind, status, 10000);
|
||||
Map<String, PriceChangeGroup> grouped = new LinkedHashMap<String, PriceChangeGroup>();
|
||||
for (BillSyncSnapshot snap : all)
|
||||
{
|
||||
if (snap == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
JSONObject req = parseRequestObject(snap.getRequestJson());
|
||||
JSONObject keys = req == null ? null : req.getJSONObject("bizKeys");
|
||||
String productCode = keys == null ? "" : StringUtils.nvl(keys.getString("productCode"), "");
|
||||
String time = StringUtils.nvl(snap.getUpdateTime(), "");
|
||||
String key = productCode + "|" + time;
|
||||
PriceChangeGroup group = grouped.get(key);
|
||||
if (group == null)
|
||||
{
|
||||
group = new PriceChangeGroup();
|
||||
group.setProductCode(StringUtils.isEmpty(productCode) ? "-" : productCode);
|
||||
group.setUpdateTime(time);
|
||||
group.setAction(req == null ? null : req.getString("action"));
|
||||
group.setOperator(req == null ? null : req.getString("operator"));
|
||||
group.setStatus(snap.getStatus());
|
||||
group.setChangeCount(0);
|
||||
grouped.put(key, group);
|
||||
}
|
||||
group.setChangeCount(group.getChangeCount() + 1);
|
||||
if (EsBizLogSupport.STATUS_FAIL.equals(snap.getStatus()))
|
||||
{
|
||||
group.setStatus(EsBizLogSupport.STATUS_FAIL);
|
||||
}
|
||||
}
|
||||
List<PriceChangeGroup> list = new ArrayList<PriceChangeGroup>(grouped.values());
|
||||
page.setTotal(list.size());
|
||||
int from = (pageNum - 1) * pageSize;
|
||||
if (from >= list.size())
|
||||
{
|
||||
return page;
|
||||
}
|
||||
int to = Math.min(from + pageSize, list.size());
|
||||
page.setRows(new ArrayList<PriceChangeGroup>(list.subList(from, to)));
|
||||
return page;
|
||||
}
|
||||
|
||||
private List<BillSyncSnapshot> loadAllPriceChanges(Long psvId, String priceKind, String status, int max)
|
||||
{
|
||||
List<BillSyncSnapshot> all = new ArrayList<BillSyncSnapshot>();
|
||||
int batch = 1000;
|
||||
int pageNum = 1;
|
||||
while (all.size() < max)
|
||||
{
|
||||
int size = Math.min(batch, max - all.size());
|
||||
BillSearchPage chunk = search(EsLogType.PRICE_CHANGE, null, status, psvId, priceKind, null, null,
|
||||
pageNum, size);
|
||||
List<BillSyncSnapshot> rows = chunk.getRows();
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
break;
|
||||
}
|
||||
all.addAll(rows);
|
||||
if (rows.size() < size || all.size() >= chunk.getTotal())
|
||||
{
|
||||
break;
|
||||
}
|
||||
pageNum++;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
private JSONObject parseRequestObject(String requestJson)
|
||||
{
|
||||
if (StringUtils.isEmpty(requestJson))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
Object parsed = JSON.parse(requestJson);
|
||||
return parsed instanceof JSONObject ? (JSONObject) parsed : null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private BillSyncSnapshot toSnapshot(JSONObject source, String id)
|
||||
{
|
||||
if (source == null)
|
||||
@@ -249,12 +386,19 @@ public class EsBillSearchStore implements IBillSearchStore
|
||||
return snap;
|
||||
}
|
||||
|
||||
private JSONObject buildQuery(String logType, String bizId, String status)
|
||||
private JSONObject buildQuery(String logType, String bizId, String status, Long psvId, String priceKind,
|
||||
String productCode, String updateTime, String updateTimePrefix, String countryKeyword)
|
||||
{
|
||||
JSONArray filters = new JSONArray();
|
||||
addTerm(filters, "logType", logType);
|
||||
addWildcard(filters, "bizId", bizId);
|
||||
addTerm(filters, "status", status);
|
||||
addPsvIdFilter(filters, psvId);
|
||||
addTerm(filters, "request.priceKind", priceKind);
|
||||
addProductCodeFilter(filters, productCode);
|
||||
addCountryKeywordFilter(filters, countryKeyword);
|
||||
addUpdateTimeFilter(filters, updateTime);
|
||||
addUpdateTimePrefixFilter(filters, updateTimePrefix);
|
||||
if (filters.isEmpty())
|
||||
{
|
||||
JSONObject matchAll = new JSONObject();
|
||||
@@ -268,6 +412,195 @@ public class EsBillSearchStore implements IBillSearchStore
|
||||
return query;
|
||||
}
|
||||
|
||||
private void addProductCodeFilter(JSONArray filters, String productCode)
|
||||
{
|
||||
if (StringUtils.isEmpty(productCode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String code = productCode.trim();
|
||||
JSONArray should = new JSONArray();
|
||||
JSONObject termKw = new JSONObject();
|
||||
termKw.put("request.bizKeys.productCode.keyword", code);
|
||||
JSONObject wrapKw = new JSONObject();
|
||||
wrapKw.put("term", termKw);
|
||||
should.add(wrapKw);
|
||||
|
||||
JSONObject term = new JSONObject();
|
||||
term.put("request.bizKeys.productCode", code);
|
||||
JSONObject wrap = new JSONObject();
|
||||
wrap.put("term", term);
|
||||
should.add(wrap);
|
||||
|
||||
JSONObject inner = new JSONObject();
|
||||
inner.put("should", should);
|
||||
inner.put("minimum_should_match", Integer.valueOf(1));
|
||||
JSONObject boolWrap = new JSONObject();
|
||||
boolWrap.put("bool", inner);
|
||||
filters.add(boolWrap);
|
||||
}
|
||||
|
||||
/** Match country code / name (exact or contains). */
|
||||
private void addCountryKeywordFilter(JSONArray filters, String countryKeyword)
|
||||
{
|
||||
if (StringUtils.isEmpty(countryKeyword))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String kw = countryKeyword.trim();
|
||||
String wild = "*" + escapeWildcard(kw) + "*";
|
||||
JSONArray should = new JSONArray();
|
||||
addShouldTerm(should, "request.bizKeys.countryCode.keyword", kw);
|
||||
addShouldTerm(should, "request.bizKeys.countryCode", kw);
|
||||
addShouldWildcard(should, "request.bizKeys.countryCode.keyword", wild);
|
||||
addShouldWildcard(should, "request.bizKeys.countryCode", wild);
|
||||
addShouldWildcard(should, "request.bizKeys.countryName.keyword", wild);
|
||||
addShouldWildcard(should, "request.bizKeys.countryName", wild);
|
||||
JSONObject matchName = new JSONObject();
|
||||
JSONObject matchField = new JSONObject();
|
||||
matchField.put("query", kw);
|
||||
matchName.put("request.bizKeys.countryName", matchField);
|
||||
JSONObject matchWrap = new JSONObject();
|
||||
matchWrap.put("match", matchName);
|
||||
should.add(matchWrap);
|
||||
|
||||
JSONObject inner = new JSONObject();
|
||||
inner.put("should", should);
|
||||
inner.put("minimum_should_match", Integer.valueOf(1));
|
||||
JSONObject boolWrap = new JSONObject();
|
||||
boolWrap.put("bool", inner);
|
||||
filters.add(boolWrap);
|
||||
}
|
||||
|
||||
private void addShouldTerm(JSONArray should, String field, String value)
|
||||
{
|
||||
JSONObject term = new JSONObject();
|
||||
term.put(field, value);
|
||||
JSONObject wrap = new JSONObject();
|
||||
wrap.put("term", term);
|
||||
should.add(wrap);
|
||||
}
|
||||
|
||||
private void addShouldWildcard(JSONArray should, String field, String value)
|
||||
{
|
||||
JSONObject wild = new JSONObject();
|
||||
wild.put(field, value);
|
||||
JSONObject wrap = new JSONObject();
|
||||
wrap.put("wildcard", wild);
|
||||
should.add(wrap);
|
||||
}
|
||||
|
||||
private static String escapeWildcard(String raw)
|
||||
{
|
||||
if (raw == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return raw.replace("\\", "\\\\").replace("*", "\\*").replace("?", "\\?");
|
||||
}
|
||||
|
||||
private void addUpdateTimeFilter(JSONArray filters, String updateTime)
|
||||
{
|
||||
if (StringUtils.isEmpty(updateTime))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String time = updateTime.trim();
|
||||
JSONArray should = new JSONArray();
|
||||
JSONObject t1 = new JSONObject();
|
||||
t1.put("updateTime.keyword", time);
|
||||
JSONObject w1 = new JSONObject();
|
||||
w1.put("term", t1);
|
||||
should.add(w1);
|
||||
|
||||
JSONObject t2 = new JSONObject();
|
||||
t2.put("updatedAt.keyword", time);
|
||||
JSONObject w2 = new JSONObject();
|
||||
w2.put("term", t2);
|
||||
should.add(w2);
|
||||
|
||||
JSONObject t3 = new JSONObject();
|
||||
t3.put("updateTime", time);
|
||||
JSONObject w3 = new JSONObject();
|
||||
w3.put("term", t3);
|
||||
should.add(w3);
|
||||
|
||||
JSONObject t4 = new JSONObject();
|
||||
t4.put("updatedAt", time);
|
||||
JSONObject w4 = new JSONObject();
|
||||
w4.put("term", t4);
|
||||
should.add(w4);
|
||||
|
||||
JSONObject inner = new JSONObject();
|
||||
inner.put("should", should);
|
||||
inner.put("minimum_should_match", Integer.valueOf(1));
|
||||
JSONObject wrap = new JSONObject();
|
||||
wrap.put("bool", inner);
|
||||
filters.add(wrap);
|
||||
}
|
||||
|
||||
/** Match write-time by minute prefix, e.g. 2026-11-05 13:27. */
|
||||
private void addUpdateTimePrefixFilter(JSONArray filters, String updateTimePrefix)
|
||||
{
|
||||
if (StringUtils.isEmpty(updateTimePrefix))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String prefix = escapeWildcard(updateTimePrefix.trim()) + "*";
|
||||
JSONArray should = new JSONArray();
|
||||
for (String field : new String[] { "updateTime.keyword", "updatedAt.keyword", "updateTime", "updatedAt" })
|
||||
{
|
||||
JSONObject opts = new JSONObject();
|
||||
opts.put("value", prefix);
|
||||
opts.put("case_insensitive", Boolean.TRUE);
|
||||
JSONObject wildcard = new JSONObject();
|
||||
wildcard.put(field, opts);
|
||||
JSONObject wrap = new JSONObject();
|
||||
wrap.put("wildcard", wildcard);
|
||||
should.add(wrap);
|
||||
}
|
||||
JSONObject inner = new JSONObject();
|
||||
inner.put("should", should);
|
||||
inner.put("minimum_should_match", Integer.valueOf(1));
|
||||
JSONObject wrap = new JSONObject();
|
||||
wrap.put("bool", inner);
|
||||
filters.add(wrap);
|
||||
}
|
||||
|
||||
/** Prefer numeric term; also accept keyword/string mapping of bizKeys.psvId. */
|
||||
private void addPsvIdFilter(JSONArray filters, Long psvId)
|
||||
{
|
||||
if (psvId == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
JSONArray should = new JSONArray();
|
||||
JSONObject termNum = new JSONObject();
|
||||
termNum.put("request.bizKeys.psvId", psvId);
|
||||
JSONObject wrapNum = new JSONObject();
|
||||
wrapNum.put("term", termNum);
|
||||
should.add(wrapNum);
|
||||
|
||||
JSONObject termKw = new JSONObject();
|
||||
termKw.put("request.bizKeys.psvId.keyword", String.valueOf(psvId));
|
||||
JSONObject wrapKw = new JSONObject();
|
||||
wrapKw.put("term", termKw);
|
||||
should.add(wrapKw);
|
||||
|
||||
JSONObject termStr = new JSONObject();
|
||||
termStr.put("request.bizKeys.psvId", String.valueOf(psvId));
|
||||
JSONObject wrapStr = new JSONObject();
|
||||
wrapStr.put("term", termStr);
|
||||
should.add(wrapStr);
|
||||
|
||||
JSONObject inner = new JSONObject();
|
||||
inner.put("should", should);
|
||||
inner.put("minimum_should_match", Integer.valueOf(1));
|
||||
JSONObject wrap = new JSONObject();
|
||||
wrap.put("bool", inner);
|
||||
filters.add(wrap);
|
||||
}
|
||||
|
||||
private void addTerm(JSONArray filters, String field, String value)
|
||||
{
|
||||
if (StringUtils.isEmpty(value))
|
||||
@@ -297,11 +630,6 @@ public class EsBillSearchStore implements IBillSearchStore
|
||||
filters.add(wrap);
|
||||
}
|
||||
|
||||
private String escapeWildcard(String value)
|
||||
{
|
||||
return value.replace("\\", "\\\\").replace("*", "\\*").replace("?", "\\?");
|
||||
}
|
||||
|
||||
private long readTotal(Object total)
|
||||
{
|
||||
if (total instanceof JSONObject)
|
||||
|
||||
@@ -17,4 +17,48 @@ public interface IBillSearchStore
|
||||
BillSyncSnapshot getByDocId(String docId);
|
||||
|
||||
BillSearchPage search(String logType, String bizId, String status, int pageNum, int pageSize);
|
||||
|
||||
/**
|
||||
* Search with optional sales-version / priceKind filters (request.bizKeys.psvId, request.priceKind).
|
||||
*/
|
||||
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
|
||||
int pageNum, int pageSize)
|
||||
{
|
||||
return search(logType, bizId, status, psvId, priceKind, null, null, pageNum, pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search with optional productCode + updateTime (exact) / updateTimePrefix (e.g. yyyy-MM-dd HH:mm) filters.
|
||||
*/
|
||||
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
|
||||
String productCode, String updateTime, int pageNum, int pageSize)
|
||||
{
|
||||
return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, null, pageNum, pageSize);
|
||||
}
|
||||
|
||||
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
|
||||
String productCode, String updateTime, String updateTimePrefix, int pageNum, int pageSize)
|
||||
{
|
||||
return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, updateTimePrefix,
|
||||
null, pageNum, pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search with optional countryKeyword (matches bizKeys.countryCode / countryName).
|
||||
*/
|
||||
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
|
||||
String productCode, String updateTime, String updateTimePrefix, String countryKeyword,
|
||||
int pageNum, int pageSize)
|
||||
{
|
||||
return search(logType, bizId, status, pageNum, pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group PRICE_CHANGE logs by productCode + updateTime for one package version.
|
||||
*/
|
||||
default PriceChangeGroupPage searchPriceChangeGroups(Long psvId, String priceKind, String status,
|
||||
int pageNum, int pageSize)
|
||||
{
|
||||
return new PriceChangeGroupPage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,33 @@ public final class PriceChangeEsLog
|
||||
public static final String ACTION_DELETE = "DELETE";
|
||||
public static final String ACTION_IMPORT = "IMPORT";
|
||||
|
||||
/** Async import workers have no SecurityContext; pass operName via this override. */
|
||||
private static final ThreadLocal<String> OPERATOR_OVERRIDE = new ThreadLocal<String>();
|
||||
|
||||
private PriceChangeEsLog()
|
||||
{
|
||||
}
|
||||
|
||||
public static void beginOperator(String operName)
|
||||
{
|
||||
if (StringUtils.isNotEmpty(operName))
|
||||
{
|
||||
OPERATOR_OVERRIDE.set(operName.trim());
|
||||
}
|
||||
}
|
||||
|
||||
public static void endOperator()
|
||||
{
|
||||
OPERATOR_OVERRIDE.remove();
|
||||
}
|
||||
|
||||
public static String operator()
|
||||
{
|
||||
String override = OPERATOR_OVERRIDE.get();
|
||||
if (StringUtils.isNotEmpty(override))
|
||||
{
|
||||
return override;
|
||||
}
|
||||
try
|
||||
{
|
||||
String name = SecurityUtils.getUsername();
|
||||
@@ -131,11 +152,31 @@ public final class PriceChangeEsLog
|
||||
keys.put("countryCode", d.getCountryCode());
|
||||
keys.put("countryName", d.getCountryName());
|
||||
keys.put("zoneCode", d.getZoneCode());
|
||||
keys.put("zoneName", d.getZoneName());
|
||||
keys.put("weightMin", d.getWeightMin());
|
||||
keys.put("weightMax", d.getWeightMax());
|
||||
keys.put("weightLabel", weightBandOf(d));
|
||||
return keys;
|
||||
}
|
||||
|
||||
/** Display weight band for sales detail logs (e.g. 0<W≤0.5). */
|
||||
public static String weightBandOf(PrcPriceSetDetail d)
|
||||
{
|
||||
if (d == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
if (StringUtils.isNotEmpty(d.getWeightLabel()))
|
||||
{
|
||||
return d.getWeightLabel();
|
||||
}
|
||||
if (d.getWeightMin() == null && d.getWeightMax() == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return stringify(d.getWeightMin()) + "<W\u2264" + stringify(d.getWeightMax());
|
||||
}
|
||||
|
||||
public static void save(IBillSearchStore store, String bizId, String priceKind, String action,
|
||||
Map<String, Object> bizKeys, List<Map<String, String>> changes)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.bms.price.search;
|
||||
|
||||
/**
|
||||
* Price-change log group key: productCode + write time.
|
||||
*/
|
||||
public class PriceChangeGroup
|
||||
{
|
||||
private String productCode;
|
||||
private String updateTime;
|
||||
private String action;
|
||||
private String operator;
|
||||
private String status;
|
||||
private int changeCount;
|
||||
|
||||
public String getProductCode()
|
||||
{
|
||||
return productCode;
|
||||
}
|
||||
|
||||
public void setProductCode(String productCode)
|
||||
{
|
||||
this.productCode = productCode;
|
||||
}
|
||||
|
||||
public String getUpdateTime()
|
||||
{
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
public void setUpdateTime(String updateTime)
|
||||
{
|
||||
this.updateTime = updateTime;
|
||||
}
|
||||
|
||||
public String getAction()
|
||||
{
|
||||
return action;
|
||||
}
|
||||
|
||||
public void setAction(String action)
|
||||
{
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public String getOperator()
|
||||
{
|
||||
return operator;
|
||||
}
|
||||
|
||||
public void setOperator(String operator)
|
||||
{
|
||||
this.operator = operator;
|
||||
}
|
||||
|
||||
public String getStatus()
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status)
|
||||
{
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public int getChangeCount()
|
||||
{
|
||||
return changeCount;
|
||||
}
|
||||
|
||||
public void setChangeCount(int changeCount)
|
||||
{
|
||||
this.changeCount = changeCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.bms.price.search;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Paginated product+time groups of price-change logs. */
|
||||
public class PriceChangeGroupPage
|
||||
{
|
||||
private long total;
|
||||
private List<PriceChangeGroup> rows = new ArrayList<PriceChangeGroup>();
|
||||
|
||||
public long getTotal()
|
||||
{
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(long total)
|
||||
{
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public List<PriceChangeGroup> getRows()
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
|
||||
public void setRows(List<PriceChangeGroup> rows)
|
||||
{
|
||||
this.rows = rows == null ? new ArrayList<PriceChangeGroup>() : rows;
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ public interface IPrcOrderBillService
|
||||
|
||||
/**
|
||||
* Async local rebill by selected billIds or list filters. Returns taskId for progress poll.
|
||||
* Large sets: only bill_id loaded, then processed in pages of 500 with parallel workers.
|
||||
* Large sets: only bill_id loaded, then processed by continuous parallel workers.
|
||||
*/
|
||||
String submitLocalRebill(LocalRebillRequest request);
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.bms.price.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.bms.price.airbill.domain.SrvBillAirFee;
|
||||
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
|
||||
import com.bms.price.airbill.domain.SrvBillAirFeeSave;
|
||||
|
||||
/**
|
||||
* 空运提单费用维护。
|
||||
*/
|
||||
public interface ISrvBillAirFeeService
|
||||
{
|
||||
List<SrvBillAirFee> listFees(String billCode);
|
||||
|
||||
List<SrvBillAirFeeLog> listLogs(String billCode);
|
||||
|
||||
/** 只写入有变动的行,并按行记录修改前后。返回给页面的提示。 */
|
||||
String save(SrvBillAirFeeSave command);
|
||||
}
|
||||
+33
-4
@@ -183,8 +183,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
private static final int BATCH_ADD_FEE_PREVIEW_LIMIT = 20;
|
||||
private static final int CHARGE_WEIGHT_IMPORT_MAX = 5000;
|
||||
private static final int RECALC_PRICE_MAX = 5000;
|
||||
/** Soft max for one local-rebill job (id-only load + paged parallel). */
|
||||
private static final int LOCAL_REBILL_MAX = 100000;
|
||||
/** Soft max for one local-rebill job (id-only load + continuous workers). */
|
||||
private static final int LOCAL_REBILL_MAX = 200000;
|
||||
/** Soft max for one order/billing list Excel export. */
|
||||
private static final int ORDER_BILL_EXPORT_MAX = 50000;
|
||||
|
||||
@@ -681,6 +681,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
request.getCustomerCodes(), request.getProductCodes(), request.getCountryCode(),
|
||||
request.getCreditArrearsStatus(), request.getOrderStatuses(), request.getBillingFailStatuses(),
|
||||
request.getBeginBizDate(), request.getEndBizDate(),
|
||||
request.getBeginPickupDate(), request.getEndPickupDate(),
|
||||
"price.bill.recalc.filterRequired", "price.bill.recalc.scopeRequired",
|
||||
"price.bill.recalc.selectedRequired");
|
||||
if (bills.isEmpty())
|
||||
@@ -1276,6 +1277,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
request.getCustomerCodes(), request.getProductCodes(), request.getCountryCode(),
|
||||
request.getCreditArrearsStatus(), request.getOrderStatuses(), request.getBillingFailStatuses(),
|
||||
request.getBeginBizDate(), request.getEndBizDate(),
|
||||
request.getBeginPickupDate(), request.getEndPickupDate(),
|
||||
"price.bill.fee.batch.filterRequired", "price.bill.fee.batch.scopeRequired",
|
||||
"price.bill.fee.batch.selectedRequired");
|
||||
}
|
||||
@@ -1290,6 +1292,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
List<String> customerCodesRaw, List<String> productCodesRaw, String countryCodeRaw,
|
||||
String creditArrearsStatusRaw, List<Integer> orderStatusesRaw, List<Integer> billingFailStatusesRaw,
|
||||
String beginBizDateRaw, String endBizDateRaw,
|
||||
String beginPickupDateRaw, String endPickupDateRaw,
|
||||
String msgFilter, String msgScope, String msgSelected)
|
||||
{
|
||||
String scopeMode = StringUtils.nvl(scopeModeRaw, "").trim().toUpperCase();
|
||||
@@ -1311,6 +1314,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
: countryCodeRaw.trim().toUpperCase();
|
||||
String beginBizDate = StringUtils.isEmpty(beginBizDateRaw) ? null : beginBizDateRaw.trim();
|
||||
String endBizDate = StringUtils.isEmpty(endBizDateRaw) ? null : endBizDateRaw.trim();
|
||||
String beginPickupDate = StringUtils.isEmpty(beginPickupDateRaw) ? null : beginPickupDateRaw.trim();
|
||||
String endPickupDate = StringUtils.isEmpty(endPickupDateRaw) ? null : endPickupDateRaw.trim();
|
||||
boolean hasFilter = (orderNos != null && !orderNos.isEmpty())
|
||||
|| (trackingNos != null && !trackingNos.isEmpty())
|
||||
|| (transferNos != null && !transferNos.isEmpty())
|
||||
@@ -1321,7 +1326,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
|| StringUtils.isNotEmpty(creditArrearsStatus)
|
||||
|| StringUtils.isNotEmpty(countryCode)
|
||||
|| StringUtils.isNotEmpty(beginBizDate)
|
||||
|| StringUtils.isNotEmpty(endBizDate);
|
||||
|| StringUtils.isNotEmpty(endBizDate)
|
||||
|| StringUtils.isNotEmpty(beginPickupDate)
|
||||
|| StringUtils.isNotEmpty(endPickupDate);
|
||||
if (!hasFilter)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message(msgFilter));
|
||||
@@ -1356,6 +1363,14 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
{
|
||||
query.getParams().put("endBizDate", endBizDate);
|
||||
}
|
||||
if (beginPickupDate != null)
|
||||
{
|
||||
query.getParams().put("beginPickupDate", beginPickupDate);
|
||||
}
|
||||
if (endPickupDate != null)
|
||||
{
|
||||
query.getParams().put("endPickupDate", endPickupDate);
|
||||
}
|
||||
}
|
||||
List<PrcOrderBill> list = self.selectBillList(query);
|
||||
return list == null ? new ArrayList<PrcOrderBill>() : list;
|
||||
@@ -2754,6 +2769,10 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
: request.getBeginBizDate().trim();
|
||||
String endBizDate = StringUtils.isEmpty(request.getEndBizDate()) ? null
|
||||
: request.getEndBizDate().trim();
|
||||
String beginPickupDate = StringUtils.isEmpty(request.getBeginPickupDate()) ? null
|
||||
: request.getBeginPickupDate().trim();
|
||||
String endPickupDate = StringUtils.isEmpty(request.getEndPickupDate()) ? null
|
||||
: request.getEndPickupDate().trim();
|
||||
boolean hasFilter = (orderNos != null && !orderNos.isEmpty())
|
||||
|| (trackingNos != null && !trackingNos.isEmpty())
|
||||
|| (transferNos != null && !transferNos.isEmpty())
|
||||
@@ -2762,7 +2781,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
|| (productCodes != null && !productCodes.isEmpty())
|
||||
|| StringUtils.isNotEmpty(creditArrearsStatus)
|
||||
|| StringUtils.isNotEmpty(beginBizDate)
|
||||
|| StringUtils.isNotEmpty(endBizDate);
|
||||
|| StringUtils.isNotEmpty(endBizDate)
|
||||
|| StringUtils.isNotEmpty(beginPickupDate)
|
||||
|| StringUtils.isNotEmpty(endPickupDate);
|
||||
if (!hasFilter)
|
||||
{
|
||||
throw new ServiceException("请先设置列表筛选条件");
|
||||
@@ -2784,6 +2805,14 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
{
|
||||
query.getParams().put("endBizDate", endBizDate);
|
||||
}
|
||||
if (beginPickupDate != null)
|
||||
{
|
||||
query.getParams().put("beginPickupDate", beginPickupDate);
|
||||
}
|
||||
if (endPickupDate != null)
|
||||
{
|
||||
query.getParams().put("endPickupDate", endPickupDate);
|
||||
}
|
||||
List<PrcOrderBill> list = self.selectBillList(query);
|
||||
if (list == null || list.isEmpty())
|
||||
{
|
||||
|
||||
+182
-4
@@ -53,6 +53,11 @@ import com.bms.price.search.PriceChangeEsLog;
|
||||
import com.bms.price.service.IPrcPriceExcelFileService;
|
||||
import com.bms.price.service.IPrcPriceSetService;
|
||||
import com.bms.price.task.PrcPriceSetSaveAsyncRunner;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomer;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomerPriceSet;
|
||||
import com.bms.customer.customerinfo.domain.BmsPriceSetOption;
|
||||
import com.bms.customer.customerinfo.service.IBmsCustomerLogService;
|
||||
import com.bms.customer.customerinfo.service.IBmsCustomerService;
|
||||
|
||||
/**
|
||||
* 价格套餐 服务层实现
|
||||
@@ -130,6 +135,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
@Autowired
|
||||
private IBillSearchStore billSearchStore;
|
||||
|
||||
@Autowired
|
||||
private IBmsCustomerLogService customerLogService;
|
||||
|
||||
@Autowired
|
||||
private IBmsCustomerService bmsCustomerService;
|
||||
|
||||
@Override
|
||||
public PrcPriceSet selectPrcPriceSetById(Long priceSetId)
|
||||
{
|
||||
@@ -1582,6 +1593,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
|
||||
String taskId, boolean updateOnly, boolean replaceByProductCountry, String countryCode) throws Exception
|
||||
{
|
||||
// Async import has no SecurityContext; bind operName for PriceChangeEsLog.operator().
|
||||
PriceChangeEsLog.beginOperator(operName);
|
||||
try
|
||||
{
|
||||
return importDetailPricesInOperator(priceSetId, psvId, bytes, filename, operName, taskId, updateOnly,
|
||||
replaceByProductCountry, countryCode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PriceChangeEsLog.endOperator();
|
||||
}
|
||||
}
|
||||
|
||||
private String importDetailPricesInOperator(Long priceSetId, Long psvId, byte[] bytes, String filename,
|
||||
String operName, String taskId, boolean updateOnly, boolean replaceByProductCountry, String countryCode)
|
||||
throws Exception
|
||||
{
|
||||
PrcPriceSet db = requireSet(priceSetId);
|
||||
assertChildEditable(db);
|
||||
@@ -2494,7 +2522,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
try
|
||||
{
|
||||
return customerMapper.insertPrcCustomerPriceSet(customer);
|
||||
int rows = customerMapper.insertPrcCustomerPriceSet(customer);
|
||||
if (rows > 0)
|
||||
{
|
||||
logCustomerPriceSetBind(null, customer, db);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -2593,6 +2626,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
validateCustomer(row, db);
|
||||
customerMapper.updatePrcCustomerPriceSet(row);
|
||||
logCustomerPriceSetBind(existing, row, db);
|
||||
replaced++;
|
||||
}
|
||||
else
|
||||
@@ -2613,6 +2647,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
logCustomerPriceSetBind(null, row, db);
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
@@ -2640,12 +2675,30 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
assertChildEditable(db);
|
||||
customer.setPriceSetId(old.getPriceSetId());
|
||||
validateCustomer(customer, db);
|
||||
return customerMapper.updatePrcCustomerPriceSet(customer);
|
||||
int rows = customerMapper.updatePrcCustomerPriceSet(customer);
|
||||
if (rows > 0)
|
||||
{
|
||||
if (customer.getCustomerId() == null)
|
||||
{
|
||||
customer.setCustomerId(old.getCustomerId());
|
||||
}
|
||||
if (StringUtils.isEmpty(customer.getCustomerCode()))
|
||||
{
|
||||
customer.setCustomerCode(old.getCustomerCode());
|
||||
}
|
||||
if (StringUtils.isEmpty(customer.getCustomerName()))
|
||||
{
|
||||
customer.setCustomerName(old.getCustomerName());
|
||||
}
|
||||
logCustomerPriceSetBind(old, customer, db);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteCustomerByIds(Long[] customerPricesetIds)
|
||||
{
|
||||
List<PrcCustomerPriceSet> snapshots = new ArrayList<PrcCustomerPriceSet>();
|
||||
for (Long id : customerPricesetIds)
|
||||
{
|
||||
PrcCustomerPriceSet old = customerMapper.selectPrcCustomerPriceSetById(id);
|
||||
@@ -2658,8 +2711,17 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.customer.unbindAudit"));
|
||||
}
|
||||
snapshots.add(old);
|
||||
}
|
||||
return customerMapper.deletePrcCustomerPriceSetByIds(customerPricesetIds);
|
||||
int rows = customerMapper.deletePrcCustomerPriceSetByIds(customerPricesetIds);
|
||||
if (rows > 0)
|
||||
{
|
||||
for (PrcCustomerPriceSet snapshot : snapshots)
|
||||
{
|
||||
logCustomerPriceSetUnbind(snapshot);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private void replaceChildren(PrcPriceSet set)
|
||||
@@ -2730,6 +2792,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
c.setUpdateBy(set.getUpdateBy());
|
||||
customerMapper.updatePrcCustomerPriceSet(c);
|
||||
keepIds.add(hit.getCustomerPricesetId());
|
||||
logCustomerPriceSetBind(hit, c, set);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2740,6 +2803,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
keepIds.add(c.getCustomerPricesetId());
|
||||
}
|
||||
logCustomerPriceSetBind(null, c, set);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -2761,10 +2825,99 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
if (!orphanIds.isEmpty())
|
||||
{
|
||||
for (Long orphanId : orphanIds)
|
||||
{
|
||||
PrcCustomerPriceSet snapshot = byId.get(orphanId);
|
||||
if (snapshot != null)
|
||||
{
|
||||
logCustomerPriceSetUnbind(snapshot);
|
||||
}
|
||||
}
|
||||
customerMapper.deletePrcCustomerPriceSetByIds(orphanIds.toArray(new Long[0]));
|
||||
}
|
||||
}
|
||||
|
||||
/** Write customer biz log for package bind / rebind (visible in customer info Logs). */
|
||||
private void logCustomerPriceSetBind(PrcCustomerPriceSet existing, PrcCustomerPriceSet bind, PrcPriceSet set)
|
||||
{
|
||||
if (customerLogService == null || bind == null || set == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
BmsCustomer customer = resolveCustomerForLog(bind);
|
||||
if (customer == null && bind.getCustomerId() != null)
|
||||
{
|
||||
customer = new BmsCustomer();
|
||||
customer.setCustomerId(bind.getCustomerId());
|
||||
customer.setCustomerCode(bind.getCustomerCode());
|
||||
customer.setCustomerName(bind.getCustomerName());
|
||||
}
|
||||
BmsCustomerPriceSet existingLog = toBmsCustomerPriceSet(existing, resolvePriceSetName(existing));
|
||||
BmsCustomerPriceSet bindLog = toBmsCustomerPriceSet(bind, set.getPriceSetName());
|
||||
if (bindLog.getCustomerId() == null && customer != null)
|
||||
{
|
||||
bindLog.setCustomerId(customer.getCustomerId());
|
||||
}
|
||||
BmsPriceSetOption option = new BmsPriceSetOption();
|
||||
option.setPriceSetId(set.getPriceSetId());
|
||||
option.setPriceSetCode(set.getPriceSetCode());
|
||||
option.setPriceSetName(set.getPriceSetName());
|
||||
customerLogService.recordPriceSetBind(customer, existingLog, bindLog, option);
|
||||
}
|
||||
|
||||
private void logCustomerPriceSetUnbind(PrcCustomerPriceSet bind)
|
||||
{
|
||||
if (customerLogService == null || bind == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
BmsCustomerPriceSet snapshot = toBmsCustomerPriceSet(bind, resolvePriceSetName(bind));
|
||||
customerLogService.recordPriceSetUnbind(snapshot);
|
||||
}
|
||||
|
||||
private BmsCustomer resolveCustomerForLog(PrcCustomerPriceSet bind)
|
||||
{
|
||||
if (bind == null || bmsCustomerService == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (bind.getCustomerId() != null)
|
||||
{
|
||||
return bmsCustomerService.selectBmsCustomerByCustomerId(bind.getCustomerId());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolvePriceSetName(PrcCustomerPriceSet bind)
|
||||
{
|
||||
if (bind == null || bind.getPriceSetId() == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PrcPriceSet set = setMapper.selectPrcPriceSetById(bind.getPriceSetId());
|
||||
return set == null ? null : set.getPriceSetName();
|
||||
}
|
||||
|
||||
private BmsCustomerPriceSet toBmsCustomerPriceSet(PrcCustomerPriceSet src, String priceSetName)
|
||||
{
|
||||
if (src == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
BmsCustomerPriceSet dest = new BmsCustomerPriceSet();
|
||||
dest.setCustomerPricesetId(src.getCustomerPricesetId());
|
||||
dest.setPriceSetId(src.getPriceSetId());
|
||||
dest.setCustomerId(src.getCustomerId());
|
||||
dest.setCustomerCode(src.getCustomerCode());
|
||||
dest.setCustomerName(src.getCustomerName());
|
||||
dest.setBindPriority(src.getBindPriority());
|
||||
dest.setPrBegindate(src.getPrBegindate());
|
||||
dest.setPrEnddate(src.getPrEnddate());
|
||||
dest.setPriceSetNote(src.getPriceSetNote());
|
||||
dest.setPriceSetName(priceSetName);
|
||||
return dest;
|
||||
}
|
||||
|
||||
private void reportSaveProgress(PrcPriceSet set, int percent, String message)
|
||||
{
|
||||
if (set == null || StringUtils.isEmpty(set.getSaveTaskId()) || saveAsyncRunner == null)
|
||||
@@ -3465,6 +3618,22 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
fillVersionChildren(version);
|
||||
}
|
||||
// Soft-deleted EFFECT versions for the "deleted" tab (status may still be EFFECT)
|
||||
PrcPriceSetVersion dq = new PrcPriceSetVersion();
|
||||
dq.setPriceSetId(priceSetId);
|
||||
dq.setPriceType(com.bms.price.domain.PriceType.SALES);
|
||||
dq.setIsDel("1");
|
||||
List<PrcPriceSetVersion> deleted = versionMapper.selectPrcPriceSetVersionList(dq);
|
||||
if (deleted != null)
|
||||
{
|
||||
for (PrcPriceSetVersion version : deleted)
|
||||
{
|
||||
version.setProducts(new ArrayList<PrcPriceSetProduct>());
|
||||
version.setDetails(new ArrayList<PrcPriceSetDetail>());
|
||||
version.setDetailCount(Integer.valueOf(detailMapper.countByPsvIdAll(version.getPsvId())));
|
||||
versions.add(version);
|
||||
}
|
||||
}
|
||||
set.setVersions(versions);
|
||||
PrcPriceSetVersion current = pickCurrentVersion(versions);
|
||||
set.setVersion(current);
|
||||
@@ -3491,14 +3660,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PrcPriceSetVersion firstAlive = null;
|
||||
for (PrcPriceSetVersion v : versions)
|
||||
{
|
||||
if (v == null || "1".equals(v.getIsDel()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (firstAlive == null)
|
||||
{
|
||||
firstAlive = v;
|
||||
}
|
||||
if (VER_EFFECT.equals(v.getStatus()))
|
||||
{
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return versions.get(0);
|
||||
return firstAlive;
|
||||
}
|
||||
|
||||
private void saveOrUpdateVersion(PrcPriceSet set)
|
||||
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
package com.bms.price.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.bms.common.core.domain.entity.SysDictData;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.DateUtils;
|
||||
import com.bms.common.utils.PageUtils;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.price.airbill.domain.SrvBillAirFee;
|
||||
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
|
||||
import com.bms.price.airbill.domain.SrvBillAirFeeSave;
|
||||
import com.bms.price.mapper.SrvBillAirFeeMapper;
|
||||
import com.bms.price.mapper.SrvBillAirMapper;
|
||||
import com.bms.price.service.ISrvBillAirFeeService;
|
||||
import com.bms.system.service.ISysDictTypeService;
|
||||
|
||||
/**
|
||||
* 提单费用只处理本次新增、修改、删除的行。没有变化的费用不更新。
|
||||
*/
|
||||
@Service
|
||||
public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
|
||||
{
|
||||
private static final String FEE_DICT = "bms_fee_type";
|
||||
|
||||
private static final String CURRENCY_DICT = "bms_currency";
|
||||
|
||||
@Autowired
|
||||
private SrvBillAirFeeMapper feeMapper;
|
||||
|
||||
@Autowired
|
||||
private SrvBillAirMapper billMapper;
|
||||
|
||||
@Autowired
|
||||
private ISysDictTypeService dictTypeService;
|
||||
|
||||
@Override
|
||||
public List<SrvBillAirFee> listFees(String billCode)
|
||||
{
|
||||
requireBill(billCode);
|
||||
return feeMapper.selectByBillCode(billCode.trim());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SrvBillAirFeeLog> listLogs(String billCode)
|
||||
{
|
||||
requireBill(billCode);
|
||||
PageUtils.startPage();
|
||||
List<SrvBillAirFeeLog> logs = feeMapper.selectLogs(billCode.trim());
|
||||
for (SrvBillAirFeeLog log : logs)
|
||||
{
|
||||
if (log.getChangeContent() != null && !log.getChangeContent().isBlank())
|
||||
{
|
||||
log.setChanges(JSON.parseArray(log.getChangeContent(), SrvBillAirFeeLog.FieldChange.class));
|
||||
}
|
||||
}
|
||||
return logs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String save(SrvBillAirFeeSave command)
|
||||
{
|
||||
if (command == null)
|
||||
{
|
||||
throw new ServiceException("请提交要保存的费用");
|
||||
}
|
||||
String billCode = requireBill(command.getBillCode());
|
||||
List<SrvBillAirFee> inserts = command.getInserts() == null ? List.of() : command.getInserts();
|
||||
List<SrvBillAirFee> updates = command.getUpdates() == null ? List.of() : command.getUpdates();
|
||||
List<Long> deletes = command.getDeletes() == null ? List.of() : command.getDeletes();
|
||||
if (inserts.isEmpty() && updates.isEmpty() && deletes.isEmpty())
|
||||
{
|
||||
throw new ServiceException("没有需要保存的变更");
|
||||
}
|
||||
Map<String, String> feeLabels = dictLabels(FEE_DICT, "费用类型");
|
||||
Map<String, String> currencyLabels = dictLabels(CURRENCY_DICT, "币种");
|
||||
Operator operator = operator();
|
||||
Date now = DateUtils.getNowDate();
|
||||
Set<Long> seen = new HashSet<>();
|
||||
int inserted = 0;
|
||||
int updated = 0;
|
||||
int removed = 0;
|
||||
for (Long id : deletes)
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
throw new ServiceException("要删除的费用缺少编号");
|
||||
}
|
||||
if (!seen.add(id))
|
||||
{
|
||||
throw new ServiceException("同一条费用不能重复提交");
|
||||
}
|
||||
SrvBillAirFee current = liveFee(id, billCode);
|
||||
if (current == null)
|
||||
{
|
||||
throw new ServiceException("费用已删除或不属于该提单");
|
||||
}
|
||||
int rows = feeMapper.deleteFee(current.getId(), billCode, operator.account, now);
|
||||
if (rows > 0)
|
||||
{
|
||||
writeLog(billCode, id, SrvBillAirFeeLog.DELETE, current, null, feeLabels, currencyLabels, operator, now);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
for (SrvBillAirFee incoming : updates)
|
||||
{
|
||||
if (incoming == null || incoming.getId() == null)
|
||||
{
|
||||
throw new ServiceException("要修改的费用缺少编号");
|
||||
}
|
||||
if (!seen.add(incoming.getId()))
|
||||
{
|
||||
throw new ServiceException("同一条费用不能重复提交");
|
||||
}
|
||||
SrvBillAirFee current = liveFee(incoming.getId(), billCode);
|
||||
if (current == null)
|
||||
{
|
||||
throw new ServiceException("费用已删除或不属于该提单");
|
||||
}
|
||||
SrvBillAirFee next = normalize(incoming, feeLabels, currencyLabels);
|
||||
List<SrvBillAirFeeLog.FieldChange> changes = diff(current, next, feeLabels, currencyLabels);
|
||||
if (changes.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
next.setId(current.getId());
|
||||
next.setBillCode(billCode);
|
||||
next.setUpdateBy(operator.account);
|
||||
next.setUpdateTime(now);
|
||||
if (feeMapper.updateFee(next) > 0)
|
||||
{
|
||||
writeLog(billCode, current.getId(), SrvBillAirFeeLog.UPDATE, current, next, feeLabels, currencyLabels, operator, now);
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
for (SrvBillAirFee incoming : inserts)
|
||||
{
|
||||
if (incoming == null || blank(incoming.getFeeType()) && incoming.getPrice() == null && blank(incoming.getCurrency()) && blank(incoming.getRemark()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
SrvBillAirFee next = normalize(incoming, feeLabels, currencyLabels);
|
||||
next.setId(null);
|
||||
next.setBillCode(billCode);
|
||||
next.setCreateBy(operator.account);
|
||||
next.setCreateTime(now);
|
||||
next.setUpdateBy(operator.account);
|
||||
next.setUpdateTime(now);
|
||||
feeMapper.insertFee(next);
|
||||
writeLog(billCode, next.getId(), SrvBillAirFeeLog.INSERT, null, next, feeLabels, currencyLabels, operator, now);
|
||||
inserted++;
|
||||
}
|
||||
if (inserted + updated + removed == 0)
|
||||
{
|
||||
throw new ServiceException("没有需要保存的变更");
|
||||
}
|
||||
return "已保存:新增 " + inserted + " 条,修改 " + updated + " 条,删除 " + removed + " 条";
|
||||
}
|
||||
|
||||
private SrvBillAirFee liveFee(Long id, String billCode)
|
||||
{
|
||||
return feeMapper.selectLive(id, billCode);
|
||||
}
|
||||
|
||||
private SrvBillAirFee normalize(SrvBillAirFee raw, Map<String, String> feeLabels, Map<String, String> currencyLabels)
|
||||
{
|
||||
String feeType = trim(raw.getFeeType());
|
||||
String currency = trim(raw.getCurrency());
|
||||
String remark = trim(raw.getRemark());
|
||||
if (feeType.isEmpty())
|
||||
{
|
||||
throw new ServiceException("请选择费用项");
|
||||
}
|
||||
if (feeType.length() > 32 || !feeLabels.containsKey(feeType))
|
||||
{
|
||||
throw new ServiceException("费用项不在费用类型字典中");
|
||||
}
|
||||
if (raw.getPrice() == null)
|
||||
{
|
||||
throw new ServiceException("请填写价格");
|
||||
}
|
||||
if (raw.getPrice().signum() < 0)
|
||||
{
|
||||
throw new ServiceException("价格不能小于0");
|
||||
}
|
||||
if (raw.getPrice().precision() - raw.getPrice().scale() > 16)
|
||||
{
|
||||
throw new ServiceException("价格整数部分不能超过16位");
|
||||
}
|
||||
if (currency.isEmpty())
|
||||
{
|
||||
throw new ServiceException("请选择收费币种");
|
||||
}
|
||||
if (currency.length() > 16 || !currencyLabels.containsKey(currency))
|
||||
{
|
||||
throw new ServiceException("收费币种不在币种字典中");
|
||||
}
|
||||
if (remark.length() > 500)
|
||||
{
|
||||
throw new ServiceException("备注不能超过500字");
|
||||
}
|
||||
SrvBillAirFee fee = new SrvBillAirFee();
|
||||
fee.setFeeType(feeType);
|
||||
fee.setPrice(raw.getPrice());
|
||||
fee.setCurrency(currency);
|
||||
fee.setRemark(remark);
|
||||
return fee;
|
||||
}
|
||||
|
||||
private void writeLog(String billCode, Long feeId, String operation, SrvBillAirFee before, SrvBillAirFee after,
|
||||
Map<String, String> feeLabels, Map<String, String> currencyLabels, Operator operator, Date now)
|
||||
{
|
||||
List<SrvBillAirFeeLog.FieldChange> changes = diff(before, after, feeLabels, currencyLabels);
|
||||
SrvBillAirFeeLog log = new SrvBillAirFeeLog();
|
||||
log.setBillCode(billCode);
|
||||
log.setFeeId(feeId);
|
||||
log.setOperationType(operation);
|
||||
log.setSummary(cut(summary(operation, changes), 1000));
|
||||
log.setChangeContent(JSON.toJSONString(changes));
|
||||
log.setCreateBy(operator.account);
|
||||
log.setCreateByName(operator.name);
|
||||
log.setCreateTime(now);
|
||||
feeMapper.insertLog(log);
|
||||
}
|
||||
|
||||
private static List<SrvBillAirFeeLog.FieldChange> diff(SrvBillAirFee before, SrvBillAirFee after,
|
||||
Map<String, String> feeLabels, Map<String, String> currencyLabels)
|
||||
{
|
||||
List<SrvBillAirFeeLog.FieldChange> changes = new ArrayList<>();
|
||||
addChange(changes, "feeType", "费用类型", label(feeLabels, value(before, SrvBillAirFee::getFeeType)),
|
||||
label(feeLabels, value(after, SrvBillAirFee::getFeeType)));
|
||||
addChange(changes, "price", "价格", priceText(before), priceText(after));
|
||||
addChange(changes, "currency", "收费币种", label(currencyLabels, value(before, SrvBillAirFee::getCurrency)),
|
||||
label(currencyLabels, value(after, SrvBillAirFee::getCurrency)));
|
||||
addChange(changes, "remark", "备注", text(before == null ? null : before.getRemark()), text(after == null ? null : after.getRemark()));
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static void addChange(List<SrvBillAirFeeLog.FieldChange> changes, String field, String fieldLabel, String oldValue, String newValue)
|
||||
{
|
||||
String left = oldValue == null ? "" : oldValue;
|
||||
String right = newValue == null ? "" : newValue;
|
||||
if (left.equals(right))
|
||||
{
|
||||
return;
|
||||
}
|
||||
changes.add(new SrvBillAirFeeLog.FieldChange(field, fieldLabel, left, right));
|
||||
}
|
||||
|
||||
private static String summary(String operation, List<SrvBillAirFeeLog.FieldChange> changes)
|
||||
{
|
||||
String action = SrvBillAirFeeLog.INSERT.equals(operation) ? "新增费用"
|
||||
: SrvBillAirFeeLog.DELETE.equals(operation) ? "删除费用" : "修改费用";
|
||||
StringBuilder text = new StringBuilder(action);
|
||||
for (SrvBillAirFeeLog.FieldChange change : changes)
|
||||
{
|
||||
text.append(";").append(change.getFieldLabel()).append(":")
|
||||
.append(show(change.getOldValue())).append(" → ").append(show(change.getNewValue()));
|
||||
}
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
private Map<String, String> dictLabels(String dictType, String name)
|
||||
{
|
||||
List<SysDictData> rows = dictTypeService.selectDictDataByType(dictType);
|
||||
Map<String, String> labels = new LinkedHashMap<>();
|
||||
if (rows != null)
|
||||
{
|
||||
for (SysDictData row : rows)
|
||||
{
|
||||
if (row != null && row.getDictValue() != null && !row.getDictValue().isBlank())
|
||||
{
|
||||
labels.put(row.getDictValue().trim(), row.getDictLabel() == null ? row.getDictValue().trim() : row.getDictLabel().trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (labels.isEmpty())
|
||||
{
|
||||
throw new ServiceException(name + "字典还没有数据");
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
private String requireBill(String billCode)
|
||||
{
|
||||
if (billCode == null || billCode.isBlank())
|
||||
{
|
||||
throw new ServiceException("提单号不能为空");
|
||||
}
|
||||
String code = billCode.trim();
|
||||
if (billMapper.selectSrvBillAirByBillCode(code) == null)
|
||||
{
|
||||
throw new ServiceException("提单不存在");
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private static Operator operator()
|
||||
{
|
||||
SysUser user = SecurityUtils.getLoginUser().getUser();
|
||||
String account = user.getUserName() == null ? "" : user.getUserName();
|
||||
String name = user.getNickName() == null || user.getNickName().isBlank() ? account : user.getNickName();
|
||||
return new Operator(account, name);
|
||||
}
|
||||
|
||||
private static String value(SrvBillAirFee fee, java.util.function.Function<SrvBillAirFee, String> getter)
|
||||
{
|
||||
return fee == null ? "" : text(getter.apply(fee));
|
||||
}
|
||||
|
||||
private static String priceText(SrvBillAirFee fee)
|
||||
{
|
||||
if (fee == null || fee.getPrice() == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return fee.getPrice().stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
private static String label(Map<String, String> labels, String value)
|
||||
{
|
||||
if (value == null || value.isBlank())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return labels.getOrDefault(value, value);
|
||||
}
|
||||
|
||||
private static String show(String value)
|
||||
{
|
||||
return value == null || value.isBlank() ? "空" : value;
|
||||
}
|
||||
|
||||
private static String text(String value)
|
||||
{
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static String trim(String value)
|
||||
{
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static boolean blank(String value)
|
||||
{
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private static String cut(String value, int max)
|
||||
{
|
||||
if (value == null || value.length() <= max)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
return value.substring(0, max);
|
||||
}
|
||||
|
||||
private static final class Operator
|
||||
{
|
||||
private final String account;
|
||||
|
||||
private final String name;
|
||||
|
||||
private Operator(String account, String name)
|
||||
{
|
||||
this.account = account;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-40
@@ -36,8 +36,7 @@ import com.bms.price.service.IPrcOrderBillService;
|
||||
|
||||
/**
|
||||
* Async local rebill from prc_order_bill only (no Oracle / MQ).
|
||||
* Large jobs page by {@link #PAGE_SIZE}; each page runs in a short-lived pool.
|
||||
* Cancel is cooperative: current in-flight bills finish, remaining bills are skipped.
|
||||
* Continuous worker pull (no page barrier); cancel skips remaining billIds.
|
||||
*/
|
||||
@Component
|
||||
public class PrcOrderBillLocalRebillAsyncRunner
|
||||
@@ -46,13 +45,11 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
|
||||
public static final String CACHE_KEY_PREFIX = "price:billing:localRebill:task:";
|
||||
public static final int TTL_MINUTES = 120;
|
||||
/** Bill this many, wait for page done, then next page. */
|
||||
public static final int PAGE_SIZE = 500;
|
||||
public static final int MIN_THREADS = 2;
|
||||
public static final int MAX_THREADS = 16;
|
||||
public static final int MAX_THREADS = 24;
|
||||
private static final int FAIL_DETAIL_MAX = 200;
|
||||
/** Publish Redis progress at most this often while a page is running. */
|
||||
private static final int PROGRESS_EVERY_N = 20;
|
||||
/** Publish Redis progress at most this often while workers run. */
|
||||
private static final int PROGRESS_EVERY_N = 50;
|
||||
private static final long PROGRESS_MIN_INTERVAL_MS = 500L;
|
||||
|
||||
private final ConcurrentHashMap<String, AtomicBoolean> cancelFlags = new ConcurrentHashMap<String, AtomicBoolean>();
|
||||
@@ -86,10 +83,12 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
AtomicInteger success = new AtomicInteger(0);
|
||||
AtomicInteger fail = new AtomicInteger(0);
|
||||
AtomicInteger done = new AtomicInteger(0);
|
||||
AtomicInteger nextIndex = new AtomicInteger(0);
|
||||
AtomicLong lastProgressAt = new AtomicLong(0L);
|
||||
StringBuilder failTips = new StringBuilder();
|
||||
List<Map<String, Object>> failDetails = Collections.synchronizedList(new ArrayList<Map<String, Object>>());
|
||||
int threads = clampThreads(billingProperties == null ? null : billingProperties.getOrderSourceSyncThreads());
|
||||
int configured = billingProperties == null ? 16 : billingProperties.getLocalRebillThreads();
|
||||
int threads = clampThreads(Integer.valueOf(configured));
|
||||
cancelFlags.computeIfAbsent(taskId, k -> new AtomicBoolean(false));
|
||||
ExecutorService pool = newWorkerPool(threads, "local-rebill");
|
||||
try
|
||||
@@ -107,30 +106,24 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails);
|
||||
return;
|
||||
}
|
||||
boolean cancelled = false;
|
||||
for (int from = 0; from < total; from += PAGE_SIZE)
|
||||
// Continuous pull: N workers grab next billId — no page barrier idle wait.
|
||||
final List<String> ids = billIds;
|
||||
List<Callable<Boolean>> workers = new ArrayList<Callable<Boolean>>(threads);
|
||||
for (int w = 0; w < threads; w++)
|
||||
{
|
||||
if (isCancelRequested(taskId))
|
||||
workers.add(() -> {
|
||||
while (!isCancelRequested(taskId))
|
||||
{
|
||||
cancelled = true;
|
||||
break;
|
||||
}
|
||||
int to = Math.min(from + PAGE_SIZE, total);
|
||||
List<String> page = billIds.subList(from, to);
|
||||
List<Callable<Boolean>> jobs = new ArrayList<Callable<Boolean>>(page.size());
|
||||
for (String billId : page)
|
||||
{
|
||||
final String one = billId;
|
||||
jobs.add(() -> {
|
||||
if (isCancelRequested(taskId))
|
||||
{
|
||||
return Boolean.FALSE;
|
||||
int i = nextIndex.getAndIncrement();
|
||||
if (i >= total)
|
||||
{
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
String one = ids.get(i);
|
||||
try
|
||||
{
|
||||
orderBillService.rebillLocalByBillId(one);
|
||||
success.incrementAndGet();
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -139,26 +132,20 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
collectFailDetail(failDetails, one, e);
|
||||
log.warn("local rebill failed taskId={} billId={} msg={}",
|
||||
taskId, one, e.getMessage());
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
finally
|
||||
{
|
||||
int finished = done.incrementAndGet();
|
||||
maybePublishProgress(taskId, total, finished, success.get(), fail.get(), lastProgressAt);
|
||||
maybePublishProgress(taskId, total, finished, success.get(), fail.get(),
|
||||
lastProgressAt);
|
||||
}
|
||||
}
|
||||
return Boolean.FALSE;
|
||||
});
|
||||
}
|
||||
invokeAllQuiet(pool, jobs);
|
||||
if (isCancelRequested(taskId))
|
||||
{
|
||||
cancelled = true;
|
||||
break;
|
||||
}
|
||||
// Page boundary: always flush latest counts (even if throttle skipped mid-page).
|
||||
invokeAllQuiet(pool, workers);
|
||||
publishProgress(taskId, total, done.get(), success.get(), fail.get());
|
||||
lastProgressAt.set(System.currentTimeMillis());
|
||||
}
|
||||
if (cancelled || isCancelRequested(taskId))
|
||||
if (isCancelRequested(taskId))
|
||||
{
|
||||
finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails);
|
||||
return;
|
||||
@@ -170,8 +157,8 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
String doneStatus = fail.get() > 0 ? EsBizLogSupport.STATUS_FAIL : EsBizLogSupport.STATUS_SUCCESS;
|
||||
String doneErr = fail.get() > 0 ? ("fail=" + fail.get() + ", ok=" + success.get()) : null;
|
||||
saveDoneLog(taskId, request, total, success.get(), fail.get(), threads, failDetails, doneStatus, doneErr);
|
||||
log.info("local rebill done taskId={} total={} success={} fail={} threads={} pageSize={}",
|
||||
taskId, total, success.get(), fail.get(), threads, PAGE_SIZE);
|
||||
log.info("local rebill done taskId={} total={} success={} fail={} threads={}",
|
||||
taskId, total, success.get(), fail.get(), threads);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -255,7 +242,6 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
resp.put("fail", Integer.valueOf(fail));
|
||||
resp.put("success", Integer.valueOf(ok));
|
||||
resp.put("threads", Integer.valueOf(threads));
|
||||
resp.put("pageSize", Integer.valueOf(PAGE_SIZE));
|
||||
if (failDetails != null && !failDetails.isEmpty())
|
||||
{
|
||||
resp.put("failDetails", new ArrayList<Map<String, Object>>(failDetails));
|
||||
|
||||
@@ -66,14 +66,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="query.endTime != null and query.endTime != ''">
|
||||
AND o.PICKUP_DATE <= TO_DATE(#{query.endTime}, 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<choose>
|
||||
<when test="query.blPickup != null">
|
||||
AND o.BL_PICKUP = #{query.blPickup}
|
||||
</when>
|
||||
<!-- <otherwise>-->
|
||||
<!-- AND o.BL_PICKUP = 1-->
|
||||
<!-- </otherwise>-->
|
||||
</choose>
|
||||
AND o.BL_PICKUP = 1
|
||||
<if test="query.orderStatuses != null and query.orderStatuses.size() > 0">
|
||||
AND o.ORDER_STATUS IN
|
||||
<foreach collection="query.orderStatuses" item="st" open="(" separator="," close=")">
|
||||
@@ -281,14 +274,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<foreach collection="batch" item="bid" open="(" separator="," close=")">
|
||||
#{bid}
|
||||
</foreach>
|
||||
<choose>
|
||||
<when test="blPickup != null">
|
||||
AND o.BL_PICKUP = #{blPickup}
|
||||
</when>
|
||||
<!-- <otherwise>-->
|
||||
<!-- AND o.BL_PICKUP = 1-->
|
||||
<!-- </otherwise>-->
|
||||
</choose>
|
||||
AND o.BL_PICKUP = 1
|
||||
AND o.ORDER_STATUS IN (3, 4, 5, 6, 7, 8, 11)
|
||||
<if test="customerCodes != null and customerCodes.size() > 0">
|
||||
AND o.CUSTOMER_CODE IN
|
||||
|
||||
@@ -158,6 +158,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
select count(1) from prc_price_set_detail where psv_id = #{psvId} and ifnull(is_del, '0') = '0'
|
||||
</select>
|
||||
|
||||
<select id="countByPsvIdAll" parameterType="Long" resultType="int">
|
||||
select count(1) from prc_price_set_detail where psv_id = #{psvId}
|
||||
</select>
|
||||
|
||||
<select id="selectDetailOptionRows" resultMap="PrcPriceSetDetailResult">
|
||||
select distinct product_code, product_name, country_code, country_name, zone_code, zone_name
|
||||
from prc_price_set_detail
|
||||
|
||||
@@ -37,7 +37,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<select id="selectPrcPriceSetVersionList" parameterType="PrcPriceSetVersion" resultMap="PrcPriceSetVersionResult">
|
||||
<include refid="selectPrcPriceSetVersionVo"/>
|
||||
<where>
|
||||
<choose>
|
||||
<when test="isDel != null and isDel != ''">
|
||||
and ifnull(is_del, '0') = #{isDel}
|
||||
</when>
|
||||
<otherwise>
|
||||
and ifnull(is_del, '0') = '0'
|
||||
</otherwise>
|
||||
</choose>
|
||||
<if test="priceSetId != null">
|
||||
AND price_set_id = #{priceSetId}
|
||||
</if>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.bms.price.mapper.SrvBillAirFeeMapper">
|
||||
|
||||
<select id="selectByBillCode" resultType="com.bms.price.airbill.domain.SrvBillAirFee">
|
||||
select id, bill_code as billCode, fee_type as feeType, price, currency, remark,
|
||||
create_by as createBy, create_time as createTime, update_by as updateBy,
|
||||
update_time as updateTime, del_flag as delFlag
|
||||
from srv_bill_air_fee
|
||||
where bill_code = #{billCode}
|
||||
and del_flag = '0'
|
||||
order by id
|
||||
</select>
|
||||
|
||||
<select id="selectLive" resultType="com.bms.price.airbill.domain.SrvBillAirFee">
|
||||
select id, bill_code as billCode, fee_type as feeType, price, currency, remark,
|
||||
create_by as createBy, create_time as createTime, update_by as updateBy,
|
||||
update_time as updateTime, del_flag as delFlag
|
||||
from srv_bill_air_fee
|
||||
where id = #{id}
|
||||
and bill_code = #{billCode}
|
||||
and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<insert id="insertFee" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into srv_bill_air_fee (
|
||||
bill_code, fee_type, price, currency, remark,
|
||||
create_by, create_time, update_by, update_time, del_flag
|
||||
) values (
|
||||
#{billCode}, #{feeType}, #{price}, #{currency}, #{remark},
|
||||
#{createBy}, #{createTime}, #{updateBy}, #{updateTime}, '0'
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateFee">
|
||||
update srv_bill_air_fee
|
||||
set fee_type = #{feeType},
|
||||
price = #{price},
|
||||
currency = #{currency},
|
||||
remark = #{remark},
|
||||
update_by = #{updateBy},
|
||||
update_time = #{updateTime}
|
||||
where id = #{id}
|
||||
and bill_code = #{billCode}
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<update id="deleteFee">
|
||||
update srv_bill_air_fee
|
||||
set del_flag = '1',
|
||||
update_by = #{updateBy},
|
||||
update_time = #{updateTime}
|
||||
where id = #{id}
|
||||
and bill_code = #{billCode}
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<insert id="insertLog" useGeneratedKeys="true" keyProperty="logId">
|
||||
insert into srv_bill_air_fee_log (
|
||||
bill_code, fee_id, operation_type, summary, change_content,
|
||||
create_by, create_by_name, create_time
|
||||
) values (
|
||||
#{billCode}, #{feeId}, #{operationType}, #{summary}, #{changeContent},
|
||||
#{createBy}, #{createByName}, #{createTime}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<select id="selectLogs" resultType="com.bms.price.airbill.domain.SrvBillAirFeeLog">
|
||||
select log_id as logId, bill_code as billCode, fee_id as feeId, operation_type as operationType,
|
||||
summary, change_content as changeContent, create_by as createBy,
|
||||
create_by_name as createByName, create_time as createTime
|
||||
from srv_bill_air_fee_log
|
||||
where bill_code = #{billCode}
|
||||
order by log_id desc
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -1,4 +1,4 @@
|
||||
-- Price change log under system monitor
|
||||
-- Price change log: remove monitor page menu; keep query permission for package version change log
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SET @monitorParent := (
|
||||
@@ -8,29 +8,36 @@ SET @monitorParent := (
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
menu_id, menu_name, parent_id, order_num, path, component, query, route_name,
|
||||
is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark
|
||||
)
|
||||
SELECT 2350, N'价格日志', IFNULL(@monitorParent, 2), 10,
|
||||
'priceLog', 'monitor/priceLog/index', '', 'PriceLog',
|
||||
1, 0, 'C', '0', '0', 'monitor:priceLog:list', 'log', 'admin', sysdate(),
|
||||
N'销售价/产品价/红线价改前改后(ES)'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2350 OR perms = 'monitor:priceLog:list');
|
||||
-- Drop legacy monitor page menu
|
||||
DELETE FROM sys_role_menu WHERE menu_id = 2350;
|
||||
DELETE FROM sys_menu WHERE menu_id = 2350
|
||||
OR (menu_type = 'C' AND (path = 'priceLog' OR component = 'monitor/priceLog/index'));
|
||||
|
||||
-- Normalize existing permission row to button (F), no page component
|
||||
UPDATE sys_menu
|
||||
SET parent_id = IFNULL(@monitorParent, parent_id),
|
||||
menu_type = 'F',
|
||||
path = '',
|
||||
component = NULL,
|
||||
visible = '0',
|
||||
status = '0',
|
||||
menu_name = N'价格变更日志查询',
|
||||
remark = N'套餐版本变更记录等查询权限(无监控页面)'
|
||||
WHERE menu_id = 2351
|
||||
OR perms = 'monitor:priceLog:list';
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
menu_id, menu_name, parent_id, order_num, path, component,
|
||||
is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time
|
||||
is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark
|
||||
)
|
||||
SELECT 2351, N'查询', 2350, 1, '', NULL,
|
||||
1, 0, 'F', '0', '0', 'monitor:priceLog:list', '#', 'admin', sysdate()
|
||||
SELECT 2351, N'价格变更日志查询', IFNULL(@monitorParent, 2), 10, '', NULL,
|
||||
1, 0, 'F', '0', '0', 'monitor:priceLog:list', '#', 'admin', sysdate(),
|
||||
N'套餐版本变更记录等查询权限(无监控页面)'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2351);
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2351 OR perms = 'monitor:priceLog:list');
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, m.menu_id
|
||||
FROM (SELECT 2350 AS menu_id UNION SELECT 2351) m
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id
|
||||
);
|
||||
FROM sys_menu m
|
||||
WHERE m.perms = 'monitor:priceLog:list'
|
||||
AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
|
||||
|
||||
@@ -14,6 +14,27 @@ INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component,
|
||||
SELECT 2522, N'详情', 2520, 2, '', NULL, 1, 0, 'F', '0', '0', 'price:airBill:query', '#', 'admin', sysdate(), N'提单详情'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2522 OR perms = 'price:airBill:query');
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2529, N'费用', 2520, 3, '', NULL, 1, 0, 'F', '0', '0', 'price:airBill:fee', '#', 'admin', sysdate(), N'维护空运提单费用'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2529 OR perms = 'price:airBill:fee');
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, m.menu_id FROM (SELECT 2520 AS menu_id UNION SELECT 2521 UNION SELECT 2522) m
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, fee.menu_id
|
||||
FROM sys_menu fee
|
||||
WHERE fee.perms = 'price:airBill:fee'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = fee.menu_id
|
||||
);
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT DISTINCT rm.role_id, fee.menu_id
|
||||
FROM sys_role_menu rm
|
||||
JOIN sys_menu fee ON fee.perms = 'price:airBill:fee'
|
||||
WHERE rm.menu_id IN (2520, 2522)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = fee.menu_id
|
||||
);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
-- 应付账单四段合计。total_amount 为账单合计,等于 A、B、C、D 四段之和。可重复执行。
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_bill' AND COLUMN_NAME = 'amount_a'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE pay_bill ADD COLUMN amount_a decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT ''A段合计'' AFTER order_count'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_bill' AND COLUMN_NAME = 'amount_b'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE pay_bill ADD COLUMN amount_b decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT ''B段合计'' AFTER amount_a'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_bill' AND COLUMN_NAME = 'amount_c'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE pay_bill ADD COLUMN amount_c decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT ''C段合计'' AFTER amount_b'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_bill' AND COLUMN_NAME = 'amount_d'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE pay_bill ADD COLUMN amount_d decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT ''D段合计'' AFTER amount_c'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
ALTER TABLE pay_bill
|
||||
MODIFY COLUMN total_amount decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '账单合计';
|
||||
@@ -8,6 +8,15 @@ export function listPriceLog(query) {
|
||||
})
|
||||
}
|
||||
|
||||
/** Group by productCode + updateTime for a package version. */
|
||||
export function listPriceLogGroups(query) {
|
||||
return request({
|
||||
url: '/monitor/priceLog/groups',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function getPriceLog(docId) {
|
||||
return request({
|
||||
url: '/monitor/priceLog/' + encodeURIComponent(docId),
|
||||
|
||||
@@ -17,3 +17,30 @@ export function getAirBill(billCode) {
|
||||
params: { billCode }
|
||||
})
|
||||
}
|
||||
|
||||
/** 一票提单的费用明细 */
|
||||
export function listAirBillFee(billCode) {
|
||||
return request({
|
||||
url: '/price/airBill/fee/list',
|
||||
method: 'get',
|
||||
params: { billCode }
|
||||
})
|
||||
}
|
||||
|
||||
/** 提单费用操作日志,分页参数 pageNum、pageSize */
|
||||
export function listAirBillFeeLog(query) {
|
||||
return request({
|
||||
url: '/price/airBill/fee/log',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/** 只提交新增、修改、删除的费用 */
|
||||
export function saveAirBillFee(data) {
|
||||
return request({
|
||||
url: '/price/airBill/fee/save',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
@@ -187,6 +187,15 @@ export function getBill(billId) {
|
||||
})
|
||||
}
|
||||
|
||||
/** ES billing history for one bill/order */
|
||||
export function listBillHistory(billId, query) {
|
||||
return request({
|
||||
url: '/price/billing/bill/' + encodeURIComponent(billId) + '/history',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/** Refund receivable bill (billType=REFUND) */
|
||||
export function refundBill(billId) {
|
||||
return request({
|
||||
|
||||
@@ -123,11 +123,13 @@ function taskDone(task, options) {
|
||||
* @param {string} [options.filePrefix='export']
|
||||
* @param {string} [options.loadingText]
|
||||
* @param {(task: object) => void} [options.onProgress]
|
||||
* @param {number} [options.pollIntervalMs=800]
|
||||
* @param {boolean} [options.showLoading=true] full-screen ElLoading; set false when page shows its own float progress
|
||||
* @param {number} [options.pollIntervalMs=1000]
|
||||
* @param {number} [options.maxWaitMs]
|
||||
* @param {string[]} [options.successStatus]
|
||||
* @param {string[]} [options.failStatus]
|
||||
* @param {boolean} [options.showSuccessMessage=true]
|
||||
* @param {boolean} [options.showErrorMessage=true]
|
||||
*/
|
||||
export async function exportAsync(options = {}) {
|
||||
const { submitUrl, taskUrlPrefix, downloadUrlPrefix } = resolveAsyncUrls(options)
|
||||
@@ -152,11 +154,15 @@ export async function exportAsync(options = {}) {
|
||||
|
||||
const filePrefix = options.filePrefix || 'export'
|
||||
const loadingText = options.loadingText || '\u6b63\u5728\u5bfc\u51fa\uff0c\u8bf7\u7a0d\u5019'
|
||||
const pollIntervalMs = options.pollIntervalMs || 800
|
||||
const pollIntervalMs = options.pollIntervalMs || 1000
|
||||
const maxWaitMs = options.maxWaitMs || 30 * 60 * 1000
|
||||
const showSuccessMessage = options.showSuccessMessage !== false
|
||||
const showErrorMessage = options.showErrorMessage !== false
|
||||
const useLoading = options.showLoading !== false
|
||||
|
||||
const loading = ElLoading.service({ text: loadingText, background: 'rgba(0, 0, 0, 0.7)' })
|
||||
const loading = useLoading
|
||||
? ElLoading.service({ text: loadingText, background: 'rgba(0, 0, 0, 0.7)' })
|
||||
: null
|
||||
try {
|
||||
const submitRes = await request({
|
||||
url: submitUrl,
|
||||
@@ -167,15 +173,23 @@ export async function exportAsync(options = {}) {
|
||||
})
|
||||
const taskId = pickTaskId(submitRes && submitRes.data)
|
||||
if (!taskId) throw new Error('missing export taskId')
|
||||
if (typeof options.onTaskId === 'function') options.onTaskId(String(taskId))
|
||||
|
||||
const started = Date.now()
|
||||
let task = null
|
||||
let lastSig = ''
|
||||
while (Date.now() - started < maxWaitMs) {
|
||||
const hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden'
|
||||
const res = await request({ url: taskUrlPrefix + String(taskId), method: 'get' })
|
||||
task = res.data || {}
|
||||
if (typeof options.onProgress === 'function') options.onProgress(task)
|
||||
const pct = Number(task.percent)
|
||||
if (!Number.isNaN(pct) && pct >= 0) {
|
||||
const st = String(task.status || task.exportStatus || '').toUpperCase()
|
||||
const sig = [st, pct, task.message, task.rowCount].join('|')
|
||||
if (typeof options.onProgress === 'function' && sig !== lastSig) {
|
||||
lastSig = sig
|
||||
options.onProgress(task)
|
||||
}
|
||||
if (loading && !Number.isNaN(pct) && pct >= 0) {
|
||||
loading.setText(loadingText + ' ' + Math.min(100, pct) + '%')
|
||||
}
|
||||
const state = taskDone(task, options)
|
||||
@@ -183,7 +197,7 @@ export async function exportAsync(options = {}) {
|
||||
if (state === 'FAIL') {
|
||||
throw new Error(task.error || task.remark || task.message || 'export failed')
|
||||
}
|
||||
await sleep(pollIntervalMs)
|
||||
await sleep(hidden ? 2500 : pollIntervalMs)
|
||||
}
|
||||
if (!task || taskDone(task, options) !== 'SUCCESS') {
|
||||
throw new Error('export timeout')
|
||||
@@ -204,10 +218,10 @@ export async function exportAsync(options = {}) {
|
||||
if (showSuccessMessage) ElMessage.success('\u5bfc\u51fa\u6210\u529f')
|
||||
return { task, taskId }
|
||||
} catch (e) {
|
||||
ElMessage.error((e && e.message) || '\u5bfc\u51fa\u5931\u8d25')
|
||||
if (showErrorMessage) ElMessage.error((e && e.message) || '\u5bfc\u51fa\u5931\u8d25')
|
||||
return null
|
||||
} finally {
|
||||
loading.close()
|
||||
if (loading) loading.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export const BILLING_BILL_EXPORT_FIELDS = [
|
||||
{ prop: 'customerActiveStatusLabel', label: '客户状态' },
|
||||
{ prop: 'creditArrearsStatusLabel', label: '额度欠费' },
|
||||
{ prop: 'bizDate', label: '计费时间' },
|
||||
{ prop: 'pickupDate', label: '提货时间' },
|
||||
{ prop: 'pickupDate', label: '收货时间' },
|
||||
{ prop: 'belongSaler', label: '销售员' },
|
||||
{ prop: 'settlementMan', label: '结算员' },
|
||||
{ prop: 'serviceMan', label: '客服员' }
|
||||
|
||||
@@ -191,14 +191,8 @@ const orderStatusMap = {
|
||||
|
||||
const orderStatusOptions = Object.keys(orderStatusMap).map(k => ({ label: orderStatusMap[k], value: Number(k) }))
|
||||
|
||||
const blPickupOptions = [
|
||||
{ label: '已收货', value: 1 },
|
||||
{ label: '未收货', value: 0 }
|
||||
]
|
||||
|
||||
const defaultParams = {
|
||||
orderStatuses: [3, 4, 5, 6, 7, 8, 11],
|
||||
blPickup: 1
|
||||
orderStatuses: [3, 4, 5, 6, 7, 8, 11]
|
||||
}
|
||||
|
||||
const searchColumns = [
|
||||
@@ -248,14 +242,7 @@ const searchColumns = [
|
||||
collapseTags: true,
|
||||
collapseTagsTooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '收货状态',
|
||||
prop: 'blPickup',
|
||||
component: 'select',
|
||||
options: blPickupOptions,
|
||||
placeholder: '请选择'
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
const columns = [
|
||||
@@ -402,7 +389,6 @@ function queryList(params) {
|
||||
const customers = splitLines(src.customerKeywords)
|
||||
const fillQuery = { orderIdentifiers: identifiers }
|
||||
if (customers.length) fillQuery.customerKeywords = customers
|
||||
fillQuery.blPickup = src.blPickup === 0 || src.blPickup === 1 ? src.blPickup : 1
|
||||
fillQuery.pageNum = params.pageNum
|
||||
fillQuery.pageSize = params.pageSize
|
||||
return listOrderSourceByTransferNos(fillQuery)
|
||||
@@ -435,7 +421,7 @@ function openOrderCompare() {
|
||||
return
|
||||
}
|
||||
proxy.$modal.confirm(
|
||||
'将按当前客户、提货时间、订单状态和收货状态筛选,异步分页比对上游转单号是否已落库,确认继续?',
|
||||
'将按当前客户、提货时间和订单状态筛选,异步分页比对上游转单号是否已落库,确认继续?',
|
||||
'二次确认'
|
||||
).then(() => {
|
||||
// 比对只按客户+时间;不带单号框内容(回填后可能有数千转单号,带上会把比对拖死)
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:search-columns="searchColumns"
|
||||
:query="queryList"
|
||||
row-key="docId"
|
||||
>
|
||||
<template #col-priceKind="{ row }">
|
||||
<el-tag size="small" :type="kindTag(row)">{{ kindLabel(row) }}</el-tag>
|
||||
</template>
|
||||
<template #col-actionName="{ row }">
|
||||
{{ actionLabel(row) }}
|
||||
</template>
|
||||
<template #col-operator="{ row }">
|
||||
{{ operatorOf(row) }}
|
||||
</template>
|
||||
<template #col-productCode="{ row }">
|
||||
{{ productCodeOf(row) }}
|
||||
</template>
|
||||
<template #col-status="{ row }">
|
||||
<el-tag size="small" :type="statusTag(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleView(row)" v-hasPermi="['monitor:priceLog:list']">查看</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
<el-drawer v-model="open" size="760px" :title="drawerTitle" append-to-body destroy-on-close>
|
||||
<div v-loading="loading">
|
||||
<el-empty v-if="!detail" description="暂无详情" />
|
||||
<div v-else>
|
||||
<el-descriptions :column="2" border size="small" class="mb12">
|
||||
<el-descriptions-item label="价格类型">
|
||||
<el-tag size="small" :type="kindTag(detail)">{{ kindLabel(detail) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作">{{ actionLabel(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">{{ operatorOf(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag size="small" :type="statusTag(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="业务ID">{{ detail.bizId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="写入时间">{{ detail.updateTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="产品代码">{{ productCodeOf(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="国家">{{ countryOf(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detail.errorMessage" label="错误信息" :span="2">
|
||||
<span class="err-text">{{ detail.errorMessage }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="change-panel">
|
||||
<div class="change-panel__head">改前改后</div>
|
||||
<el-table :data="changesOf(detail)" border size="small" empty-text="无字段变更">
|
||||
<el-table-column prop="fieldLabel" label="字段" min-width="120" />
|
||||
<el-table-column prop="oldValue" label="改前" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="newValue" label="改后" min-width="140" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="json-panel">
|
||||
<div class="json-panel__head">
|
||||
<span>请求报文</span>
|
||||
<el-button size="small" icon="CopyDocument" @click="copyJson(detail.requestJson)">复制</el-button>
|
||||
</div>
|
||||
<pre class="code-pre">{{ formatJson(detail.requestJson) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="PriceLog">
|
||||
import { listPriceLog, getPriceLog } from '@/api/monitor/priceLog'
|
||||
|
||||
const tableRef = ref()
|
||||
const open = ref(false)
|
||||
const loading = ref(false)
|
||||
const detail = ref(null)
|
||||
const drawerTitle = ref('价格日志详情')
|
||||
|
||||
const kindOptions = [
|
||||
{ label: '销售价', value: 'SALES' },
|
||||
{ label: '产品价', value: 'PRODUCT' },
|
||||
{ label: '红线价', value: 'REDLINE' }
|
||||
]
|
||||
|
||||
const actionOptions = [
|
||||
{ label: '新增', value: 'INSERT' },
|
||||
{ label: '修改', value: 'UPDATE' },
|
||||
{ label: '删除', value: 'DELETE' },
|
||||
{ label: '导入', value: 'IMPORT' }
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '成功', value: 'SUCCESS' },
|
||||
{ label: '失败', value: 'FAIL' }
|
||||
]
|
||||
|
||||
const searchColumns = [
|
||||
{ label: '状态', prop: 'status', component: 'select', options: statusOptions, placeholder: '请选择状态', width: '140px', props: { clearable: true } },
|
||||
{ label: '业务ID', prop: 'bizId', component: 'input', placeholder: 'SALES_/PRODUCT_/REDLINE_', width: '240px' }
|
||||
]
|
||||
|
||||
const columns = [
|
||||
{ label: '价格类型', prop: 'priceKind', align: 'center', width: 100, slot: 'priceKind' },
|
||||
{ label: '操作', prop: 'actionName', align: 'center', width: 90, slot: 'actionName' },
|
||||
{ label: '操作人', prop: 'operator', width: 110, slot: 'operator', showOverflowTooltip: true },
|
||||
{ label: '产品代码', prop: 'productCode', minWidth: 120, slot: 'productCode', showOverflowTooltip: true },
|
||||
{ label: '状态', prop: 'status', align: 'center', width: 90, slot: 'status' },
|
||||
{ label: '业务ID', prop: 'bizId', minWidth: 160, showOverflowTooltip: true },
|
||||
{ label: '写入时间', prop: 'updateTime', align: 'center', width: 180 },
|
||||
{ label: '操作', prop: 'action', align: 'center', width: 90, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
|
||||
function queryList(params) {
|
||||
return listPriceLog(params)
|
||||
}
|
||||
|
||||
function parseReq(row) {
|
||||
if (!row || !row.requestJson) return {}
|
||||
try {
|
||||
return typeof row.requestJson === 'object' ? row.requestJson : JSON.parse(row.requestJson)
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function kindOf(row) {
|
||||
const req = parseReq(row)
|
||||
return req.priceKind || ''
|
||||
}
|
||||
|
||||
function kindLabel(row) {
|
||||
const v = kindOf(row)
|
||||
const found = kindOptions.find(i => i.value === v)
|
||||
return found ? found.label : (v || '-')
|
||||
}
|
||||
|
||||
function kindTag(row) {
|
||||
const v = kindOf(row)
|
||||
if (v === 'SALES') return 'success'
|
||||
if (v === 'PRODUCT') return 'warning'
|
||||
if (v === 'REDLINE') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function actionLabel(row) {
|
||||
const req = parseReq(row)
|
||||
const found = actionOptions.find(i => i.value === req.action)
|
||||
return found ? found.label : (req.action || '-')
|
||||
}
|
||||
|
||||
function operatorOf(row) {
|
||||
return parseReq(row).operator || '-'
|
||||
}
|
||||
|
||||
function productCodeOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
return keys.productCode || '-'
|
||||
}
|
||||
|
||||
function countryOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
if (keys.countryCode && keys.countryName) return keys.countryName + ' (' + keys.countryCode + ')'
|
||||
return keys.countryCode || keys.countryName || '-'
|
||||
}
|
||||
|
||||
function changesOf(row) {
|
||||
const req = parseReq(row)
|
||||
return Array.isArray(req.changes) ? req.changes : []
|
||||
}
|
||||
|
||||
function statusLabel(status) {
|
||||
const found = statusOptions.find(item => item.value === status)
|
||||
if (found) return found.label
|
||||
return status === 'FAIL' ? '失败' : '成功'
|
||||
}
|
||||
|
||||
function statusTag(status) {
|
||||
return status === 'FAIL' ? 'danger' : 'success'
|
||||
}
|
||||
|
||||
function handleView(row) {
|
||||
drawerTitle.value = '价格日志 #' + (row.bizId || row.docId)
|
||||
open.value = true
|
||||
loading.value = true
|
||||
detail.value = null
|
||||
getPriceLog(row.docId).then(res => {
|
||||
detail.value = res.data || row
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function formatJson(val) {
|
||||
if (val === null || val === undefined || val === '') return '无数据'
|
||||
if (typeof val === 'object') {
|
||||
try { return JSON.stringify(val, null, 2) } catch (e) { return String(val) }
|
||||
}
|
||||
try { return JSON.stringify(JSON.parse(val), null, 2) } catch (e) { return String(val) }
|
||||
}
|
||||
|
||||
function copyJson(val) {
|
||||
const text = formatJson(val)
|
||||
const done = () => ElMessage({ message: '已复制', type: 'success', duration: 1500 })
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(done)
|
||||
return
|
||||
}
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
done()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.err-text { color: #f56c6c; word-break: break-all; }
|
||||
.change-panel {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid #e8ecf0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.change-panel__head {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
background: #fafbfc;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.json-panel {
|
||||
margin-bottom: 12px;
|
||||
background: #f7f9fb;
|
||||
border: 1px solid #e8ecf0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.json-panel__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
background: #fafbfc;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.json-panel .code-pre {
|
||||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -49,18 +49,22 @@
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" v-hasPermi="['price:airBill:query']" @click="openDetail(row)">详情</el-button>
|
||||
<el-button link type="primary" v-hasPermi="['price:airBill:fee']" @click="openFee(row)">费用</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
<air-bill-detail v-model="open" :title="title" :loading="loading" :detail="detail" />
|
||||
<air-bill-fee-dialog ref="feeRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="AirBill">
|
||||
import { listAirBill, getAirBill } from '@/api/price/airBill'
|
||||
import AirBillDetail from './components/AirBillDetail.vue'
|
||||
import AirBillFeeDialog from './components/AirBillFeeDialog.vue'
|
||||
|
||||
const tableRef = ref()
|
||||
const feeRef = ref()
|
||||
const tableHeight = ref(480)
|
||||
let tableResizeObserver = null
|
||||
|
||||
@@ -102,7 +106,7 @@ const columns = [
|
||||
{ label: '到达时间', prop: 'arriveTime', width: 170, parseTime: true },
|
||||
{ label: '到仓时间', prop: 'arriveStockTime', width: 170, parseTime: true },
|
||||
{ label: '清关时间', prop: 'clearanceTime', width: 170, parseTime: true },
|
||||
{ label: '操作', prop: 'action', align: 'center', width: 90, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
|
||||
{ label: '操作', prop: 'action', align: 'center', width: 130, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
|
||||
]
|
||||
|
||||
const searchColumns = [
|
||||
@@ -163,6 +167,10 @@ function queryList(params) {
|
||||
return listAirBill(normalizeQuery(params))
|
||||
}
|
||||
|
||||
function openFee(row) {
|
||||
feeRef.value?.open(row)
|
||||
}
|
||||
|
||||
function openDetail(row) {
|
||||
if (!row?.billCode) return
|
||||
open.value = true
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
<template>
|
||||
<div class="app-container billing-bill">
|
||||
<div v-if="exportFloatVisible" class="retry-status-float export-status-float">
|
||||
<el-tooltip :content="exportFloatTooltip" placement="bottom">
|
||||
<el-button class="retry-status-button" type="warning" plain size="small" @click="openExportProgress()">
|
||||
<span class="retry-progress-ring" :style="{ '--retry-progress': exportProgressDegree + 'deg' }" aria-hidden="true">
|
||||
<span class="retry-progress-value">{{ exportProgressText }}</span>
|
||||
</span>
|
||||
<span class="retry-status-text">
|
||||
<strong>{{ exportFloatTitle }}</strong>
|
||||
<small>{{ exportFloatSummary }}</small>
|
||||
</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
@@ -157,6 +172,8 @@
|
||||
type="warning"
|
||||
plain
|
||||
icon="Download"
|
||||
:loading="exportLoading"
|
||||
:disabled="exportLoading"
|
||||
v-hasPermi="['price:billing:export']"
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
@@ -268,6 +285,32 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="exportProgressVisible"
|
||||
:title="exportDialogTitle"
|
||||
width="480px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="true"
|
||||
:show-close="true"
|
||||
>
|
||||
<el-progress
|
||||
:percentage="exportProgressPercent"
|
||||
:status="exportProgressStatus"
|
||||
:stroke-width="14"
|
||||
striped
|
||||
:striped-flow="exportLoading"
|
||||
/>
|
||||
<div class="export-progress__msg">{{ exportProgressMessage }}</div>
|
||||
<div class="export-progress__text">
|
||||
导出行数 {{ exportProgressRowCount }}
|
||||
</div>
|
||||
<p v-if="exportLoading" class="export-progress__hint">导出在后台进行,可收起弹窗继续操作;请勿关闭或刷新本页</p>
|
||||
<template #footer>
|
||||
<el-button v-if="exportLoading" @click="exportProgressVisible = false">收起</el-button>
|
||||
<el-button v-else type="primary" @click="exportProgressVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<excel-export-field-dialog
|
||||
ref="exportFieldRef"
|
||||
:fields="BILLING_BILL_EXPORT_FIELDS"
|
||||
@@ -322,6 +365,78 @@ const productOptions = computed(() =>
|
||||
.map(p => ({ code: p.productCode, name: p.productName || p.productCode }))
|
||||
)
|
||||
|
||||
const exportLoading = ref(false)
|
||||
const exportFinished = ref(false)
|
||||
const exportProgressVisible = ref(false)
|
||||
const exportProgressPercent = ref(0)
|
||||
const exportProgressMessage = ref('')
|
||||
const exportProgressStatus = ref(undefined)
|
||||
const exportProgressRowCount = ref(0)
|
||||
const exportTaskId = ref('')
|
||||
let exportFloatHideTimer = null
|
||||
|
||||
const exportFloatVisible = computed(() => exportLoading.value || exportFinished.value)
|
||||
const exportProgressDegree = computed(() => Math.round(((exportProgressPercent.value || 0) / 100) * 360))
|
||||
const exportProgressText = computed(() => (exportProgressPercent.value || 0) + '%')
|
||||
const exportDialogTitle = computed(() => {
|
||||
if (exportLoading.value) return '异步导出进行中'
|
||||
if (exportFinished.value) return '异步导出完成'
|
||||
return '异步导出'
|
||||
})
|
||||
const exportFloatTitle = computed(() => {
|
||||
if (exportLoading.value) return '导出中'
|
||||
if (exportProgressStatus.value === 'exception') return '导出失败'
|
||||
if (exportFinished.value) return '导出完成'
|
||||
return '导出'
|
||||
})
|
||||
const exportFloatSummary = computed(() => {
|
||||
const pct = exportProgressPercent.value || 0
|
||||
const rows = exportProgressRowCount.value || 0
|
||||
if (rows > 0) return pct + '% / ' + rows + ' 行'
|
||||
return pct + '%'
|
||||
})
|
||||
const exportFloatTooltip = computed(() => exportProgressMessage.value || '点击查看详情')
|
||||
|
||||
function openExportProgress() {
|
||||
exportProgressVisible.value = true
|
||||
}
|
||||
|
||||
function scheduleHideExportFloat() {
|
||||
if (exportFloatHideTimer) {
|
||||
clearTimeout(exportFloatHideTimer)
|
||||
exportFloatHideTimer = null
|
||||
}
|
||||
exportFloatHideTimer = window.setTimeout(() => {
|
||||
if (!exportLoading.value && exportFinished.value) {
|
||||
exportFinished.value = false
|
||||
}
|
||||
exportFloatHideTimer = null
|
||||
}, 8000)
|
||||
}
|
||||
|
||||
function applyExportProgress(t) {
|
||||
const pct = Math.min(100, Number(t && t.percent) || 0)
|
||||
exportProgressPercent.value = pct
|
||||
exportProgressRowCount.value = Number(t && t.rowCount) || 0
|
||||
const st = String((t && t.status) || '').toUpperCase()
|
||||
if (st === 'PENDING') {
|
||||
exportProgressMessage.value = (t && t.message) || '排队中...'
|
||||
exportProgressStatus.value = undefined
|
||||
} else if (st === 'RUNNING') {
|
||||
exportProgressMessage.value = (t && t.message) || ('导出中 ' + pct + '%')
|
||||
exportProgressStatus.value = undefined
|
||||
} else if (st === 'SUCCESS') {
|
||||
exportProgressMessage.value = (t && t.message) || '导出完成,正在下载'
|
||||
exportProgressStatus.value = 'success'
|
||||
exportProgressPercent.value = 100
|
||||
} else if (st === 'FAIL' || st === 'FAILED') {
|
||||
exportProgressMessage.value = (t && (t.error || t.message)) || '导出失败'
|
||||
exportProgressStatus.value = 'exception'
|
||||
} else {
|
||||
exportProgressMessage.value = (t && t.message) || ''
|
||||
}
|
||||
}
|
||||
|
||||
const tableRef = ref()
|
||||
const exportFieldRef = ref()
|
||||
const tableHeight = ref(480)
|
||||
@@ -460,7 +575,7 @@ const columns = [
|
||||
{ label: '额度欠费', prop: 'creditArrearsStatus', align: 'center', width: 100, slot: 'creditArrearsStatus' },
|
||||
// { label: '退费状态', prop: 'refundStatus', align: 'center', width: 100, slot: 'refundStatus' },
|
||||
{ label: '计费时间', prop: 'bizDate', align: 'center', width: 170, parseTime: true },
|
||||
{ label: '提货时间', prop: 'pickupDate', align: 'center', width: 170, parseTime: true },
|
||||
{ label: '收货时间', prop: 'pickupDate', align: 'center', width: 170, parseTime: true },
|
||||
{ label: '销售员', prop: 'belongSaler', minWidth: 100, showOverflowTooltip: true },
|
||||
{ label: '结算员', prop: 'settlementMan', minWidth: 100, showOverflowTooltip: true },
|
||||
{ label: '客服员', prop: 'serviceMan', minWidth: 100, showOverflowTooltip: true },
|
||||
@@ -534,20 +649,35 @@ const searchColumns = [
|
||||
// 只选日期时自动补全为当天 00:00:00 ~ 23:59:59
|
||||
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '收货时间',
|
||||
prop: 'pickupDateRange',
|
||||
component: 'datetimerange',
|
||||
colSpan: 2,
|
||||
props: {
|
||||
clearable: true,
|
||||
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
function buildBillQuery(params) {
|
||||
const { bizDateRange, orderIdentifiers: rawOrderIdentifiers, ...rest } = params || {}
|
||||
const { bizDateRange, pickupDateRange, orderIdentifiers: rawOrderIdentifiers, ...rest } = params || {}
|
||||
const query = { ...rest }
|
||||
const orderIdentifiers = parseOrderIdentifiers(rawOrderIdentifiers)
|
||||
if (orderIdentifiers.length) query.orderIdentifiers = orderIdentifiers
|
||||
const rangeParams = {}
|
||||
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
|
||||
query.params = {
|
||||
...(query.params || {}),
|
||||
beginBizDate: bizDateRange[0] || null,
|
||||
endBizDate: bizDateRange[1] || null
|
||||
rangeParams.beginBizDate = bizDateRange[0] || null
|
||||
rangeParams.endBizDate = bizDateRange[1] || null
|
||||
}
|
||||
if (Array.isArray(pickupDateRange) && (pickupDateRange[0] || pickupDateRange[1])) {
|
||||
rangeParams.beginPickupDate = pickupDateRange[0] || null
|
||||
rangeParams.endPickupDate = pickupDateRange[1] || null
|
||||
}
|
||||
if (Object.keys(rangeParams).length) {
|
||||
query.params = { ...(query.params || {}), ...rangeParams }
|
||||
}
|
||||
return query
|
||||
}
|
||||
@@ -561,11 +691,25 @@ function queryList(params) {
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
if (exportLoading.value) return
|
||||
const params = tableRef.value?.getQueryParams?.() || {}
|
||||
if (!validateOrderIdentifierLimit(params.orderIdentifiers)) return
|
||||
const fields = await exportFieldRef.value?.open?.()
|
||||
if (!fields || !fields.length) return
|
||||
return exportAsyncFromTable({
|
||||
if (exportFloatHideTimer) {
|
||||
clearTimeout(exportFloatHideTimer)
|
||||
exportFloatHideTimer = null
|
||||
}
|
||||
exportLoading.value = true
|
||||
exportFinished.value = false
|
||||
exportProgressVisible.value = false
|
||||
exportProgressPercent.value = 0
|
||||
exportProgressRowCount.value = 0
|
||||
exportProgressStatus.value = undefined
|
||||
exportProgressMessage.value = '导出任务已提交'
|
||||
exportTaskId.value = ''
|
||||
try {
|
||||
const result = await exportAsyncFromTable({
|
||||
baseUrl: '/price/billing/bill/export',
|
||||
tableRef: tableRef.value,
|
||||
normalize: (raw) => {
|
||||
@@ -574,8 +718,39 @@ async function handleExport() {
|
||||
return q
|
||||
},
|
||||
filePrefix: 'billing_bill',
|
||||
requireFilter: true
|
||||
requireFilter: true,
|
||||
showLoading: false,
|
||||
showSuccessMessage: true,
|
||||
showErrorMessage: true,
|
||||
onTaskId: (id) => { exportTaskId.value = id },
|
||||
onProgress: (task) => applyExportProgress(task || {})
|
||||
})
|
||||
if (result && result.task) {
|
||||
applyExportProgress({ ...(result.task || {}), status: 'SUCCESS', percent: 100 })
|
||||
exportFinished.value = true
|
||||
scheduleHideExportFloat()
|
||||
} else if (!exportTaskId.value) {
|
||||
exportFinished.value = false
|
||||
exportProgressPercent.value = 0
|
||||
exportProgressMessage.value = ''
|
||||
exportProgressStatus.value = undefined
|
||||
} else if (exportProgressStatus.value !== 'exception') {
|
||||
exportProgressStatus.value = 'exception'
|
||||
exportProgressMessage.value = exportProgressMessage.value || '导出失败'
|
||||
exportFinished.value = true
|
||||
scheduleHideExportFloat()
|
||||
} else {
|
||||
exportFinished.value = true
|
||||
scheduleHideExportFloat()
|
||||
}
|
||||
} catch (e) {
|
||||
exportProgressStatus.value = 'exception'
|
||||
exportProgressMessage.value = (e && e.message) || '导出失败'
|
||||
exportFinished.value = true
|
||||
scheduleHideExportFloat()
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -666,14 +841,15 @@ function hasBatchFeeQuery(q) {
|
||||
|| hasArr(q.productCodes)
|
||||
|| hasArr(q.billingFailStatuses)
|
||||
|| hasArr(q.bizDateRange)
|
||||
|| hasArr(q.pickupDateRange)
|
||||
|| !!(q.creditArrearsStatus && String(q.creditArrearsStatus).trim())
|
||||
}
|
||||
|
||||
function openBatchFee() {
|
||||
const q = tableRef.value?.getQueryParams?.() || {}
|
||||
if (!validateOrderIdentifierLimit(q.orderIdentifiers)) return
|
||||
const { pageNum, pageSize, bizDateRange, ...rest } = q
|
||||
if (!hasBatchFeeQuery({ ...rest, bizDateRange })) {
|
||||
const { pageNum, pageSize, bizDateRange, pickupDateRange, ...rest } = q
|
||||
if (!hasBatchFeeQuery({ ...rest, bizDateRange, pickupDateRange })) {
|
||||
ElMessage.warning('请先设置列表查询条件')
|
||||
return
|
||||
}
|
||||
@@ -685,6 +861,10 @@ function openBatchFee() {
|
||||
listQuery.beginBizDate = bizDateRange[0] || null
|
||||
listQuery.endBizDate = bizDateRange[1] || null
|
||||
}
|
||||
if (Array.isArray(pickupDateRange) && (pickupDateRange[0] || pickupDateRange[1])) {
|
||||
listQuery.beginPickupDate = pickupDateRange[0] || null
|
||||
listQuery.endPickupDate = pickupDateRange[1] || null
|
||||
}
|
||||
batchFeeListQuery.value = listQuery
|
||||
batchFeeSelectedIds.value = tableRef.value?.getSelectionIds?.() || []
|
||||
batchFeeOpen.value = true
|
||||
@@ -697,8 +877,8 @@ function onBatchFeeSuccess() {
|
||||
function buildRecalcPayload() {
|
||||
const q = tableRef.value?.getQueryParams?.() || {}
|
||||
if (!validateOrderIdentifierLimit(q.orderIdentifiers)) return null
|
||||
const { pageNum, pageSize, bizDateRange, ...rest } = q
|
||||
if (!hasBatchFeeQuery({ ...rest, bizDateRange })) {
|
||||
const { pageNum, pageSize, bizDateRange, pickupDateRange, ...rest } = q
|
||||
if (!hasBatchFeeQuery({ ...rest, bizDateRange, pickupDateRange })) {
|
||||
ElMessage.warning('请先设置列表查询条件')
|
||||
return null
|
||||
}
|
||||
@@ -720,6 +900,10 @@ function buildRecalcPayload() {
|
||||
payload.beginBizDate = bizDateRange[0] || null
|
||||
payload.endBizDate = bizDateRange[1] || null
|
||||
}
|
||||
if (Array.isArray(pickupDateRange) && (pickupDateRange[0] || pickupDateRange[1])) {
|
||||
payload.beginPickupDate = pickupDateRange[0] || null
|
||||
payload.endPickupDate = pickupDateRange[1] || null
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
@@ -796,7 +980,7 @@ function onChargeWeightUpdated() {
|
||||
|
||||
function buildChargeWeightTemplateQuery() {
|
||||
const q = tableRef.value?.getQueryParams?.() || {}
|
||||
const { pageNum, pageSize, bizDateRange, ...rest } = q
|
||||
const { pageNum, pageSize, bizDateRange, pickupDateRange, ...rest } = q
|
||||
const payload = {
|
||||
fillByFilter: !!cwFillByFilter.value,
|
||||
orderNos: rest.orderNos || [],
|
||||
@@ -812,6 +996,10 @@ function buildChargeWeightTemplateQuery() {
|
||||
payload.beginBizDate = bizDateRange[0] || null
|
||||
payload.endBizDate = bizDateRange[1] || null
|
||||
}
|
||||
if (Array.isArray(pickupDateRange) && (pickupDateRange[0] || pickupDateRange[1])) {
|
||||
payload.beginPickupDate = pickupDateRange[0] || null
|
||||
payload.endPickupDate = pickupDateRange[1] || null
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
@@ -1020,4 +1208,67 @@ function refundTagType(status) {
|
||||
width: 100%;
|
||||
}
|
||||
.order-identifier-search { width: 100%; }
|
||||
.billing-bill .retry-status-float {
|
||||
position: fixed;
|
||||
top: 92px;
|
||||
right: 24px;
|
||||
z-index: 3001;
|
||||
}
|
||||
.billing-bill .retry-status-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 200px;
|
||||
min-height: 68px;
|
||||
padding: 6px 16px 6px 7px;
|
||||
border-radius: 36px;
|
||||
}
|
||||
.billing-bill .retry-progress-ring {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 54px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(
|
||||
var(--el-color-warning) 0deg var(--retry-progress, 45deg),
|
||||
var(--el-color-warning-light-8) var(--retry-progress, 45deg) 360deg
|
||||
);
|
||||
}
|
||||
.billing-bill .retry-progress-ring::after {
|
||||
position: absolute;
|
||||
inset: 5px;
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
.billing-bill .retry-progress-value {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--el-color-warning);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
.billing-bill .retry-status-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.billing-bill .retry-status-text strong {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.billing-bill .retry-status-text small {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
.export-progress__msg { margin-top: 12px; color: var(--el-text-color-regular); }
|
||||
.export-progress__text { margin-top: 8px; font-size: 13px; color: var(--el-text-color-secondary); }
|
||||
.export-progress__hint { margin-top: 12px; font-size: 12px; color: var(--el-color-warning); }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="980px" append-to-body destroy-on-close @closed="reset">
|
||||
<div v-loading="loading">
|
||||
<div class="fee-toolbar">
|
||||
<el-button type="primary" plain icon="Plus" @click="addRow">新增</el-button>
|
||||
<span class="fee-hint">只保存本次新增、修改和删除的费用,未改动的行不会重写。</span>
|
||||
</div>
|
||||
<el-table :data="rows" border size="small" max-height="320" row-key="rowKey" empty-text="暂无费用,点击新增">
|
||||
<el-table-column label="费用项" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.feeType" filterable clearable placeholder="请选择" style="width: 100%">
|
||||
<el-option v-for="item in bms_fee_type" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.price" :min="0" :precision="4" :controls="false" :value-on-clear="null" placeholder="价格" style="width: 100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收费币种" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.currency" filterable clearable placeholder="请选择" style="width: 100%">
|
||||
<el-option v-for="item in bms_currency" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.remark" maxlength="500" clearable placeholder="备注" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" @click="removeRow(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="log-title">操作日志</div>
|
||||
<div v-loading="logLoading">
|
||||
<el-table :data="logs" border size="small" max-height="320" empty-text="暂无操作记录">
|
||||
<el-table-column label="操作时间" width="168">
|
||||
<template #default="{ row }">{{ logTime(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作类型" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="logType(row.operationType)">{{ logAction(row.operationType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更改前" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-if="blankSide(row, 'old')" class="log-empty">空</div>
|
||||
<div v-else class="log-change">
|
||||
<div v-for="change in row.changes" :key="change.field">{{ change.fieldLabel || change.field }}:{{ showValue(change.oldValue) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更改后" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-if="blankSide(row, 'new')" class="log-empty">空</div>
|
||||
<div v-else class="log-change">
|
||||
<div v-for="change in row.changes" :key="change.field">{{ change.fieldLabel || change.field }}:{{ showValue(change.newValue) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.createByName || row.createBy || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="logPageNum"
|
||||
v-model:page-size="logPageSize"
|
||||
class="log-pager"
|
||||
:total="logTotal"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
small
|
||||
@current-change="loadLogs"
|
||||
@size-change="changeLogSize"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!dirty" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listAirBillFee, listAirBillFeeLog, saveAirBillFee } from '@/api/price/airBill'
|
||||
import { parseTime } from '@/utils/bms'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const logLoading = ref(false)
|
||||
const saving = ref(false)
|
||||
const billCode = ref('')
|
||||
const rows = ref([])
|
||||
const deletedIds = ref([])
|
||||
const logs = ref([])
|
||||
const logPageNum = ref(1)
|
||||
const logPageSize = ref(10)
|
||||
const logTotal = ref(0)
|
||||
let rowSeed = 0
|
||||
let logSeq = 0
|
||||
|
||||
const title = computed(() => billCode.value ? `提单费用 - ${billCode.value}` : '提单费用')
|
||||
const dirty = computed(() => deletedIds.value.length > 0 || rows.value.some(row => rowChanged(row)))
|
||||
|
||||
function open(row) {
|
||||
if (!row?.billCode) return
|
||||
billCode.value = row.billCode
|
||||
visible.value = true
|
||||
load()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (visible.value) return
|
||||
billCode.value = ''
|
||||
rows.value = []
|
||||
deletedIds.value = []
|
||||
logs.value = []
|
||||
logPageNum.value = 1
|
||||
logPageSize.value = 10
|
||||
logTotal.value = 0
|
||||
logSeq += 1
|
||||
saving.value = false
|
||||
loading.value = false
|
||||
logLoading.value = false
|
||||
}
|
||||
|
||||
function load() {
|
||||
logPageNum.value = 1
|
||||
loadFees()
|
||||
loadLogs()
|
||||
}
|
||||
|
||||
function loadFees() {
|
||||
const code = billCode.value
|
||||
loading.value = true
|
||||
listAirBillFee(code).then(feeRes => {
|
||||
if (code !== billCode.value) return
|
||||
deletedIds.value = []
|
||||
rows.value = (feeRes.data || []).map(item => ({
|
||||
rowKey: `fee-${item.id}`,
|
||||
id: item.id,
|
||||
feeType: item.feeType == null ? '' : String(item.feeType),
|
||||
price: item.price === null || item.price === undefined ? null : Number(item.price),
|
||||
currency: item.currency || '',
|
||||
remark: item.remark || '',
|
||||
origin: snapshot(item)
|
||||
}))
|
||||
}).finally(() => {
|
||||
if (code === billCode.value) loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function unwrapLogs(res) {
|
||||
const rows = res?.rows
|
||||
if (Array.isArray(rows)) {
|
||||
const total = Number(res.total)
|
||||
return { list: rows, total: Number.isFinite(total) ? total : rows.length }
|
||||
}
|
||||
if (rows && Array.isArray(rows.list)) {
|
||||
const total = Number(rows.total ?? res.total)
|
||||
return { list: rows.list, total: Number.isFinite(total) ? total : rows.list.length }
|
||||
}
|
||||
if (Array.isArray(res?.data)) {
|
||||
return { list: res.data, total: res.data.length }
|
||||
}
|
||||
return { list: [], total: 0 }
|
||||
}
|
||||
|
||||
function loadLogs() {
|
||||
const code = billCode.value
|
||||
if (!code) return
|
||||
const seq = ++logSeq
|
||||
logLoading.value = true
|
||||
listAirBillFeeLog({
|
||||
billCode: code,
|
||||
pageNum: logPageNum.value,
|
||||
pageSize: logPageSize.value
|
||||
}).then(logRes => {
|
||||
if (seq !== logSeq || code !== billCode.value) return
|
||||
const page = unwrapLogs(logRes)
|
||||
logs.value = page.list
|
||||
logTotal.value = page.total
|
||||
}).finally(() => {
|
||||
if (seq === logSeq && code === billCode.value) logLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function changeLogSize() {
|
||||
logPageNum.value = 1
|
||||
loadLogs()
|
||||
}
|
||||
|
||||
function addRow() {
|
||||
rowSeed += 1
|
||||
rows.value.push({
|
||||
rowKey: `new-${rowSeed}`,
|
||||
id: null,
|
||||
feeType: '',
|
||||
price: null,
|
||||
currency: '',
|
||||
remark: '',
|
||||
origin: null
|
||||
})
|
||||
}
|
||||
|
||||
function removeRow(row) {
|
||||
if (row.id != null && !deletedIds.value.some(id => String(id) === String(row.id))) {
|
||||
deletedIds.value.push(row.id)
|
||||
}
|
||||
rows.value = rows.value.filter(item => item.rowKey !== row.rowKey)
|
||||
}
|
||||
|
||||
function snapshot(item) {
|
||||
return {
|
||||
feeType: item.feeType || '',
|
||||
price: normPrice(item.price),
|
||||
currency: item.currency || '',
|
||||
remark: (item.remark || '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
function currentOf(row) {
|
||||
return {
|
||||
feeType: row.feeType || '',
|
||||
price: normPrice(row.price),
|
||||
currency: row.currency || '',
|
||||
remark: (row.remark || '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
function normPrice(value) {
|
||||
if (value === null || value === undefined || value === '') return ''
|
||||
const number = Number(value)
|
||||
return Number.isNaN(number) ? String(value).trim() : String(number)
|
||||
}
|
||||
|
||||
function rowChanged(row) {
|
||||
if (!row.origin) {
|
||||
const now = currentOf(row)
|
||||
return !!(now.feeType || now.price || now.currency || now.remark)
|
||||
}
|
||||
const now = currentOf(row)
|
||||
return now.feeType !== row.origin.feeType || now.price !== row.origin.price
|
||||
|| now.currency !== row.origin.currency || now.remark !== row.origin.remark
|
||||
}
|
||||
|
||||
function save() {
|
||||
const inserts = []
|
||||
const updates = []
|
||||
rows.value.forEach(row => {
|
||||
if (!rowChanged(row)) return
|
||||
const payload = {
|
||||
feeType: row.feeType || '',
|
||||
price: row.price,
|
||||
currency: row.currency || '',
|
||||
remark: (row.remark || '').trim()
|
||||
}
|
||||
if (row.id) updates.push({ id: row.id, ...payload })
|
||||
else inserts.push(payload)
|
||||
})
|
||||
if (!inserts.length && !updates.length && !deletedIds.value.length) {
|
||||
proxy.$modal.msgError('没有需要保存的变更')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
saveAirBillFee({ billCode: billCode.value, inserts, updates, deletes: deletedIds.value }).then(res => {
|
||||
proxy.$modal.msgSuccess(res.msg || '保存成功')
|
||||
load()
|
||||
}).catch(() => {
|
||||
load()
|
||||
}).finally(() => {
|
||||
saving.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function logAction(type) {
|
||||
return { 1: '新增', 2: '修改', 3: '删除' }[String(type)] || '维护'
|
||||
}
|
||||
|
||||
function logType(type) {
|
||||
if (String(type) === '1') return 'success'
|
||||
if (String(type) === '3') return 'danger'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function logTime(item) {
|
||||
return parseTime(item.createTime) || ''
|
||||
}
|
||||
|
||||
function showValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '空' : value
|
||||
}
|
||||
|
||||
function blankSide(row, side) {
|
||||
const changes = row.changes || []
|
||||
if (!changes.length) return true
|
||||
const key = side === 'old' ? 'oldValue' : 'newValue'
|
||||
return changes.every(change => showValue(change[key]) === '空')
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fee-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.fee-hint {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.log-title {
|
||||
margin: 16px 0 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.log-change {
|
||||
line-height: 1.7;
|
||||
white-space: normal;
|
||||
}
|
||||
.log-empty {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.log-pager {
|
||||
margin-top: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="modelValue"
|
||||
:title="title"
|
||||
size="860px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<div class="hist-wrap" v-loading="loading">
|
||||
<el-table :data="rows" border height="calc(100vh - 220px)" :empty-text="emptyText">
|
||||
<el-table-column :label="colTime" width="170" align="center" prop="updateTime" />
|
||||
<el-table-column :label="colAction" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="actionTag(row)">{{ actionLabel(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colStatus" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="statusTag(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colSet" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ priceSetOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colVer" width="90" align="center">
|
||||
<template #default="{ row }">{{ versionOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colPsd" width="100" align="center">
|
||||
<template #default="{ row }">{{ psdOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colAmount" width="110" align="right">
|
||||
<template #default="{ row }">{{ amountOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colOps" width="80" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetail(row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
v-model:current-page="query.pageNum"
|
||||
v-model:page-size="query.pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="reload"
|
||||
@current-change="reload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="detailOpen" size="720px" :title="detailTitle" append-to-body destroy-on-close>
|
||||
<div v-if="!active">
|
||||
<el-empty :description="emptyDetail" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-descriptions :column="2" border size="small" class="mb12">
|
||||
<el-descriptions-item :label="colTime">{{ active.updateTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="colAction">{{ actionLabel(active) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="colStatus">{{ statusLabel(active.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="colSet">{{ priceSetOf(active) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="colVer">{{ versionOf(active) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="colPsd">{{ psdOf(active) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="colAmount">{{ amountOf(active) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbMatch">{{ matchMsgOf(active) }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="active.errorMessage" :label="lbError" :span="2">
|
||||
<span class="err">{{ active.errorMessage }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="json-panel">
|
||||
<div class="json-panel__head">请求报文</div>
|
||||
<pre class="code-pre">{{ formatJson(active.requestJson) }}</pre>
|
||||
</div>
|
||||
<div class="json-panel">
|
||||
<div class="json-panel__head">返回报文</div>
|
||||
<pre class="code-pre">{{ formatJson(active.responseJson) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup name="BillBillingHistoryDrawer">
|
||||
import { listBillHistory } from '@/api/price/billing'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
billId: { type: String, default: '' },
|
||||
orderNo: { type: String, default: '' }
|
||||
})
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const query = reactive({ pageNum: 1, pageSize: 10 })
|
||||
const detailOpen = ref(false)
|
||||
const active = ref(null)
|
||||
|
||||
const emptyText = '暂无历史计费记录'
|
||||
const emptyDetail = '暂无详情'
|
||||
const colTime = '写入时间'
|
||||
const colAction = '类型'
|
||||
const colStatus = '状态'
|
||||
const colSet = '套餐'
|
||||
const colVer = '版本'
|
||||
const colPsd = '明细ID'
|
||||
const colAmount = '应收总额'
|
||||
const colOps = '操作'
|
||||
const lbMatch = '命中说明'
|
||||
const lbError = '错误信息'
|
||||
|
||||
const title = computed(() => {
|
||||
const no = props.orderNo || ''
|
||||
return no ? '历史计费记录 · ' + no : '历史计费记录'
|
||||
})
|
||||
const detailTitle = computed(() => '计费记录详情')
|
||||
|
||||
function parseJson(val) {
|
||||
if (val == null || val === '') return {}
|
||||
if (typeof val === 'object') return val
|
||||
try { return JSON.parse(val) } catch (e) { return {} }
|
||||
}
|
||||
|
||||
function respOf(row) {
|
||||
return parseJson(row && row.responseJson)
|
||||
}
|
||||
|
||||
function reqOf(row) {
|
||||
return parseJson(row && row.requestJson)
|
||||
}
|
||||
|
||||
function actionLabel(row) {
|
||||
const req = reqOf(row)
|
||||
if (req.action === 'UPSTREAM_SYNC') return '上游同步'
|
||||
if (row && row.status === 'FAIL') return '计费失败'
|
||||
const resp = respOf(row)
|
||||
if (resp.matchSource === 'BILLING_FAIL' || resp.billingFailStatus) return '计费失败'
|
||||
return '计费'
|
||||
}
|
||||
|
||||
function actionTag(row) {
|
||||
const label = actionLabel(row)
|
||||
if (label === '上游同步') return 'info'
|
||||
if (label === '计费失败') return 'danger'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
function statusLabel(status) {
|
||||
return status === 'FAIL' ? '失败' : '成功'
|
||||
}
|
||||
|
||||
function statusTag(status) {
|
||||
return status === 'FAIL' ? 'danger' : 'success'
|
||||
}
|
||||
|
||||
function priceSetOf(row) {
|
||||
const r = respOf(row)
|
||||
if (r.priceSetName && r.priceSetCode) return r.priceSetName + ' (' + r.priceSetCode + ')'
|
||||
return r.priceSetName || r.priceSetCode || (r.priceSetId != null ? String(r.priceSetId) : '-')
|
||||
}
|
||||
|
||||
function versionOf(row) {
|
||||
const r = respOf(row)
|
||||
if (r.versionNo) return r.versionNo
|
||||
return r.psvId != null ? ('psv=' + r.psvId) : '-'
|
||||
}
|
||||
|
||||
function psdOf(row) {
|
||||
const r = respOf(row)
|
||||
return r.psdId != null ? String(r.psdId) : '-'
|
||||
}
|
||||
|
||||
function amountOf(row) {
|
||||
const r = respOf(row)
|
||||
const v = r.incomeTotal != null ? r.incomeTotal : r.payableTotal
|
||||
if (v == null || v === '') return '-'
|
||||
const n = Number(v)
|
||||
return Number.isNaN(n) ? String(v) : n.toFixed(2)
|
||||
}
|
||||
|
||||
function matchMsgOf(row) {
|
||||
return respOf(row).matchMessage || '-'
|
||||
}
|
||||
|
||||
function formatJson(val) {
|
||||
if (val == null || val === '') return '无数据'
|
||||
if (typeof val === 'object') {
|
||||
try { return JSON.stringify(val, null, 2) } catch (e) { return String(val) }
|
||||
}
|
||||
try { return JSON.stringify(JSON.parse(val), null, 2) } catch (e) { return String(val) }
|
||||
}
|
||||
|
||||
function openDetail(row) {
|
||||
active.value = row
|
||||
detailOpen.value = true
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
if (!props.billId) {
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listBillHistory(props.billId, {
|
||||
pageNum: query.pageNum,
|
||||
pageSize: query.pageSize
|
||||
})
|
||||
rows.value = res.rows || []
|
||||
total.value = Number(res.total) || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.billId],
|
||||
([open]) => {
|
||||
if (open) {
|
||||
query.pageNum = 1
|
||||
detailOpen.value = false
|
||||
active.value = null
|
||||
reload()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hist-wrap { min-height: 200px; }
|
||||
.pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.err { color: #f56c6c; word-break: break-all; }
|
||||
.json-panel {
|
||||
margin-bottom: 12px;
|
||||
background: #f7f9fb;
|
||||
border: 1px solid #e8ecf0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.json-panel__head {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
background: #fafbfc;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.code-pre {
|
||||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -41,7 +41,17 @@
|
||||
</section>
|
||||
|
||||
<section class="detail-block">
|
||||
<div class="sec-title">计费信息</div>
|
||||
<div class="sec-title">
|
||||
计费信息
|
||||
<el-button
|
||||
v-if="detail && detail.billId"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
class="hist-btn"
|
||||
@click="historyOpen = true"
|
||||
>历史计费</el-button>
|
||||
</div>
|
||||
<div class="amount-summary">
|
||||
<div class="amount-card">
|
||||
<div class="amount-label">运费金额</div>
|
||||
@@ -187,6 +197,11 @@
|
||||
:current-charge-weight="detail?.chargeWeight"
|
||||
@success="onChargeWeightUpdated"
|
||||
/>
|
||||
<bill-billing-history-drawer
|
||||
v-model="historyOpen"
|
||||
:bill-id="detail?.billId || ''"
|
||||
:order-no="detail?.orderNo || ''"
|
||||
/>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
@@ -196,6 +211,8 @@ import EditChargeWeightDialog from './EditChargeWeightDialog.vue'
|
||||
import { deleteIncomeBillFee, getBill } from '@/api/price/billing'
|
||||
import { generatePayBill } from '@/api/price/pay'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import BillBillingHistoryDrawer from './BillBillingHistoryDrawer.vue'
|
||||
import { deleteIncomeBillFee } from '@/api/price/billing'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
@@ -210,6 +227,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:modelValue', 'updated'])
|
||||
|
||||
const feeTab = ref('income')
|
||||
const historyOpen = ref(false)
|
||||
const addFeeOpen = ref(false)
|
||||
const editFeeRow = ref(null)
|
||||
const editCwOpen = ref(false)
|
||||
@@ -229,6 +247,7 @@ const payFees = computed(() => {
|
||||
|
||||
watch(() => props.detail, () => {
|
||||
feeTab.value = 'income'
|
||||
historyOpen.value = false
|
||||
})
|
||||
|
||||
function handlePayBill() {
|
||||
@@ -493,6 +512,9 @@ function refundLabel(status) {
|
||||
border-radius: 2px;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
.hist-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
.amount-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
<template>
|
||||
<div class="app-container order-bill-query">
|
||||
<div v-if="exportFloatVisible" class="retry-status-float export-status-float" :class="{ 'is-offset': rebillFloatVisible }">
|
||||
<el-tooltip :content="exportFloatTooltip" placement="bottom">
|
||||
<el-button class="retry-status-button" type="warning" plain size="small" @click="openExportProgress()">
|
||||
<span class="retry-progress-ring" :style="{ '--retry-progress': exportProgressDegree + 'deg' }" aria-hidden="true">
|
||||
<span class="retry-progress-value">{{ exportProgressText }}</span>
|
||||
</span>
|
||||
<span class="retry-status-text">
|
||||
<strong>{{ exportFloatTitle }}</strong>
|
||||
<small>{{ exportFloatSummary }}</small>
|
||||
</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="rebillFloatVisible" class="retry-status-float">
|
||||
<el-tooltip :content="rebillFloatTooltip" placement="bottom">
|
||||
<el-button class="retry-status-button" type="primary" plain size="small" @click="openRebillProgress()">
|
||||
@@ -58,6 +72,8 @@
|
||||
type="warning"
|
||||
plain
|
||||
icon="Download"
|
||||
:loading="exportLoading"
|
||||
:disabled="exportLoading"
|
||||
v-hasPermi="['price:orderBill:export']"
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
@@ -208,6 +224,32 @@
|
||||
<el-button v-else type="primary" @click="rebillProgressVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="exportProgressVisible"
|
||||
:title="exportDialogTitle"
|
||||
width="480px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="true"
|
||||
:show-close="true"
|
||||
>
|
||||
<el-progress
|
||||
:percentage="exportProgressPercent"
|
||||
:status="exportProgressStatus"
|
||||
:stroke-width="14"
|
||||
striped
|
||||
:striped-flow="exportLoading"
|
||||
/>
|
||||
<div class="rebill-progress__msg">{{ exportProgressMessage }}</div>
|
||||
<div class="rebill-progress__text">
|
||||
导出行数 {{ exportProgressRowCount }}
|
||||
</div>
|
||||
<p v-if="exportLoading" class="rebill-progress__hint">导出在后台进行,可收起弹窗继续操作;请勿关闭或刷新本页</p>
|
||||
<template #footer>
|
||||
<el-button v-if="exportLoading" @click="exportProgressVisible = false">收起</el-button>
|
||||
<el-button v-else type="primary" @click="exportProgressVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<order-source-sync-log-drawer ref="logDrawerRef" />
|
||||
<order-bill-product-check-drawer ref="productCheckDrawerRef" />
|
||||
|
||||
@@ -233,6 +275,78 @@ const exportFieldRef = ref()
|
||||
const logDrawerRef = ref()
|
||||
const productCheckDrawerRef = ref()
|
||||
const checkLoading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const exportFinished = ref(false)
|
||||
const exportProgressVisible = ref(false)
|
||||
const exportProgressPercent = ref(0)
|
||||
const exportProgressMessage = ref('')
|
||||
const exportProgressStatus = ref(undefined)
|
||||
const exportProgressRowCount = ref(0)
|
||||
const exportTaskId = ref('')
|
||||
let exportFloatHideTimer = null
|
||||
|
||||
const exportFloatVisible = computed(() => exportLoading.value || exportFinished.value)
|
||||
const exportProgressDegree = computed(() => Math.round(((exportProgressPercent.value || 0) / 100) * 360))
|
||||
const exportProgressText = computed(() => (exportProgressPercent.value || 0) + '%')
|
||||
const exportDialogTitle = computed(() => {
|
||||
if (exportLoading.value) return '异步导出进行中'
|
||||
if (exportFinished.value) return '异步导出完成'
|
||||
return '异步导出'
|
||||
})
|
||||
const exportFloatTitle = computed(() => {
|
||||
if (exportLoading.value) return '导出中'
|
||||
if (exportProgressStatus.value === 'exception') return '导出失败'
|
||||
if (exportFinished.value) return '导出完成'
|
||||
return '导出'
|
||||
})
|
||||
const exportFloatSummary = computed(() => {
|
||||
const pct = exportProgressPercent.value || 0
|
||||
const rows = exportProgressRowCount.value || 0
|
||||
if (rows > 0) return pct + '% / ' + rows + ' 行'
|
||||
return pct + '%'
|
||||
})
|
||||
const exportFloatTooltip = computed(() => exportProgressMessage.value || '点击查看详情')
|
||||
|
||||
function openExportProgress() {
|
||||
exportProgressVisible.value = true
|
||||
}
|
||||
|
||||
function scheduleHideExportFloat() {
|
||||
if (exportFloatHideTimer) {
|
||||
clearTimeout(exportFloatHideTimer)
|
||||
exportFloatHideTimer = null
|
||||
}
|
||||
exportFloatHideTimer = window.setTimeout(() => {
|
||||
if (!exportLoading.value && exportFinished.value) {
|
||||
exportFinished.value = false
|
||||
}
|
||||
exportFloatHideTimer = null
|
||||
}, 8000)
|
||||
}
|
||||
|
||||
function applyExportProgress(t) {
|
||||
const pct = Math.min(100, Number(t && t.percent) || 0)
|
||||
exportProgressPercent.value = pct
|
||||
exportProgressRowCount.value = Number(t && t.rowCount) || 0
|
||||
const st = String((t && t.status) || '').toUpperCase()
|
||||
if (st === 'PENDING') {
|
||||
exportProgressMessage.value = (t && t.message) || '排队中...'
|
||||
exportProgressStatus.value = undefined
|
||||
} else if (st === 'RUNNING') {
|
||||
exportProgressMessage.value = (t && t.message) || ('导出中 ' + pct + '%')
|
||||
exportProgressStatus.value = undefined
|
||||
} else if (st === 'SUCCESS') {
|
||||
exportProgressMessage.value = (t && t.message) || '导出完成,正在下载'
|
||||
exportProgressStatus.value = 'success'
|
||||
exportProgressPercent.value = 100
|
||||
} else if (st === 'FAIL' || st === 'FAILED') {
|
||||
exportProgressMessage.value = (t && (t.error || t.message)) || '导出失败'
|
||||
exportProgressStatus.value = 'exception'
|
||||
} else {
|
||||
exportProgressMessage.value = (t && t.message) || ''
|
||||
}
|
||||
}
|
||||
|
||||
const batchLoading = ref(false)
|
||||
const rebillFinished = ref(false)
|
||||
const rebillProgressVisible = ref(false)
|
||||
@@ -516,11 +630,25 @@ function queryList(params) {
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
if (exportLoading.value) return
|
||||
const params = tableRef.value?.getQueryParams?.() || {}
|
||||
if (!validateOrderIdentifierLimit(params.orderIdentifiers)) return
|
||||
const fields = await exportFieldRef.value?.open?.()
|
||||
if (!fields || !fields.length) return
|
||||
return exportAsyncFromTable({
|
||||
if (exportFloatHideTimer) {
|
||||
clearTimeout(exportFloatHideTimer)
|
||||
exportFloatHideTimer = null
|
||||
}
|
||||
exportLoading.value = true
|
||||
exportFinished.value = false
|
||||
exportProgressVisible.value = false
|
||||
exportProgressPercent.value = 0
|
||||
exportProgressRowCount.value = 0
|
||||
exportProgressStatus.value = undefined
|
||||
exportProgressMessage.value = '导出任务已提交'
|
||||
exportTaskId.value = ''
|
||||
try {
|
||||
const result = await exportAsyncFromTable({
|
||||
baseUrl: '/price/billing/orderBill/export',
|
||||
tableRef: tableRef.value,
|
||||
normalize: (raw) => {
|
||||
@@ -529,8 +657,40 @@ async function handleExport() {
|
||||
return q
|
||||
},
|
||||
filePrefix: 'order_bill',
|
||||
requireFilter: true
|
||||
requireFilter: true,
|
||||
showLoading: false,
|
||||
showSuccessMessage: true,
|
||||
showErrorMessage: true,
|
||||
onTaskId: (id) => { exportTaskId.value = id },
|
||||
onProgress: (task) => applyExportProgress(task || {})
|
||||
})
|
||||
if (result && result.task) {
|
||||
applyExportProgress({ ...(result.task || {}), status: 'SUCCESS', percent: 100 })
|
||||
exportFinished.value = true
|
||||
scheduleHideExportFloat()
|
||||
} else if (!exportTaskId.value) {
|
||||
// filter rejected / cancelled before task created
|
||||
exportFinished.value = false
|
||||
exportProgressPercent.value = 0
|
||||
exportProgressMessage.value = ''
|
||||
exportProgressStatus.value = undefined
|
||||
} else if (exportProgressStatus.value !== 'exception') {
|
||||
exportProgressStatus.value = 'exception'
|
||||
exportProgressMessage.value = exportProgressMessage.value || '导出失败'
|
||||
exportFinished.value = true
|
||||
scheduleHideExportFloat()
|
||||
} else {
|
||||
exportFinished.value = true
|
||||
scheduleHideExportFloat()
|
||||
}
|
||||
} catch (e) {
|
||||
exportProgressStatus.value = 'exception'
|
||||
exportProgressMessage.value = (e && e.message) || '导出失败'
|
||||
exportFinished.value = true
|
||||
scheduleHideExportFloat()
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -889,6 +1049,12 @@ onBeforeUnmount(() => {
|
||||
.rebill-progress__msg { margin-top: 12px; color: var(--el-text-color-regular); }
|
||||
.rebill-progress__text { margin-top: 8px; font-size: 13px; color: var(--el-text-color-secondary); }
|
||||
.rebill-progress__hint { margin-top: 12px; font-size: 12px; color: var(--el-color-warning); }
|
||||
.order-bill-query .export-status-float {
|
||||
top: 92px;
|
||||
}
|
||||
.order-bill-query .export-status-float.is-offset {
|
||||
top: 178px;
|
||||
}
|
||||
.order-bill-query .retry-status-float {
|
||||
position: fixed;
|
||||
top: 92px;
|
||||
|
||||
@@ -0,0 +1,567 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="visible"
|
||||
:title="title"
|
||||
size="900px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@close="close"
|
||||
>
|
||||
<div class="price-log-drawer" v-loading="loading">
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="query.productKeyword"
|
||||
clearable
|
||||
:placeholder="phProduct"
|
||||
style="width: 220px"
|
||||
@keyup.enter="reload"
|
||||
/>
|
||||
<el-button type="primary" @click="reload">查询</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!timeTabs.length" :description="emptyText" />
|
||||
<template v-else>
|
||||
<el-tabs v-model="activeTime" class="time-tabs" @tab-change="onTimeTabChange">
|
||||
<el-tab-pane
|
||||
v-for="t in timeTabs"
|
||||
:key="t.key"
|
||||
:name="t.key"
|
||||
:label="t.label"
|
||||
/>
|
||||
</el-tabs>
|
||||
|
||||
<el-table :data="productRows" border height="calc(100vh - 320px)" :empty-text="emptyProducts">
|
||||
<el-table-column :label="colProduct" min-width="120" prop="productCode" show-overflow-tooltip />
|
||||
<el-table-column :label="colAction" width="90" align="center">
|
||||
<template #default="{ row }">{{ actionLabelOf(row.action) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colOperator" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.operator || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colCount" width="100" align="center" prop="changeCount" />
|
||||
<el-table-column :label="colOps" width="90" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openProduct(row)">明细</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="groupOpen" size="860px" :title="groupTitle" append-to-body destroy-on-close>
|
||||
<div v-loading="groupLoading">
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="groupQuery.countryKeyword"
|
||||
clearable
|
||||
:placeholder="phCountry"
|
||||
style="width: 220px"
|
||||
@keyup.enter="searchGroup"
|
||||
/>
|
||||
<el-button type="primary" @click="searchGroup">查询</el-button>
|
||||
<el-button @click="resetGroupQuery">重置</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="displayGroupRows"
|
||||
border
|
||||
height="calc(100vh - 260px)"
|
||||
:empty-text="emptyGroup"
|
||||
:span-method="groupSpanMethod"
|
||||
>
|
||||
<el-table-column :label="colCountry" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ countryOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colZone" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ zoneOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colWeight" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ weightBandOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colAction" width="90" align="center">
|
||||
<template #default="{ row }">{{ actionLabel(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colChangeSummary" min-width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ changeSummary(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colTime" width="170" align="center" prop="updateTime" />
|
||||
<el-table-column :label="colOps" width="90" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
v-model:current-page="groupQuery.pageNum"
|
||||
v-model:page-size="groupQuery.pageSize"
|
||||
:total="groupTotal"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="reloadGroup"
|
||||
@current-change="reloadGroup"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer v-model="detailOpen" size="720px" :title="detailTitle" append-to-body destroy-on-close>
|
||||
<div v-loading="detailLoading">
|
||||
<el-empty v-if="!detail" :description="emptyDetail" />
|
||||
<div v-else>
|
||||
<el-descriptions :column="2" border size="small" class="mb12">
|
||||
<el-descriptions-item :label="lbAction">{{ actionLabel(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbOperator">{{ operatorOf(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbStatus">
|
||||
<el-tag size="small" :type="statusTag(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbTime">{{ detail.updateTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbProduct">{{ productCodeOf(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbCountry">{{ countryOf(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbWeight">{{ weightBandOf(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbZone">{{ zoneOf(detail) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="change-panel">
|
||||
<div class="change-panel__head">改前改后</div>
|
||||
<el-table :data="changesOf(detail)" border size="small" :empty-text="emptyChanges">
|
||||
<el-table-column prop="fieldLabel" :label="colField" min-width="120" />
|
||||
<el-table-column prop="oldValue" :label="colOld" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="newValue" :label="colNew" min-width="140" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup name="SetVersionPriceLogDrawer">
|
||||
import { listPriceLog, listPriceLogGroups, getPriceLog } from '@/api/monitor/priceLog'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
version: { type: Object, default: null }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: v => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const allGroups = ref([])
|
||||
const query = reactive({ productKeyword: '' })
|
||||
const activeTime = ref('')
|
||||
|
||||
const groupOpen = ref(false)
|
||||
const groupLoading = ref(false)
|
||||
const groupRows = ref([])
|
||||
const groupTotal = ref(0)
|
||||
const groupTitle = ref('')
|
||||
const activeProduct = ref(null)
|
||||
const groupQuery = reactive({ pageNum: 1, pageSize: 20, countryKeyword: '' })
|
||||
|
||||
const detailOpen = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detail = ref(null)
|
||||
const detailTitle = ref('')
|
||||
|
||||
const phProduct = '产品代码'
|
||||
const phCountry = '国家代码/名称'
|
||||
const emptyText = '该版本暂无价格变更记录'
|
||||
const emptyProducts = '该时间点暂无产品变更'
|
||||
const emptyGroup = '该产品暂无明细变更'
|
||||
const emptyDetail = '暂无详情'
|
||||
const emptyChanges = '无字段变更'
|
||||
const colAction = '操作'
|
||||
const colOperator = '操作人'
|
||||
const colProduct = '产品代码'
|
||||
const colCountry = '国家'
|
||||
const colZone = '分区'
|
||||
const colWeight = '公斤段'
|
||||
const colCount = '变更条数'
|
||||
const colChangeSummary = '变更摘要'
|
||||
const colStatus = '状态'
|
||||
const colTime = '写入时间'
|
||||
const colOps = '操作'
|
||||
const colField = '字段'
|
||||
const colOld = '改前'
|
||||
const colNew = '改后'
|
||||
const lbAction = '操作'
|
||||
const lbOperator = '操作人'
|
||||
const lbStatus = '状态'
|
||||
const lbTime = '写入时间'
|
||||
const lbProduct = '产品代码'
|
||||
const lbCountry = '国家'
|
||||
const lbWeight = '公斤段'
|
||||
const lbZone = '分区'
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '成功', value: 'SUCCESS' },
|
||||
{ label: '失败', value: 'FAIL' }
|
||||
]
|
||||
const actionOptions = [
|
||||
{ label: '新增', value: 'INSERT' },
|
||||
{ label: '修改', value: 'UPDATE' },
|
||||
{ label: '删除', value: 'DELETE' },
|
||||
{ label: '导入', value: 'IMPORT' }
|
||||
]
|
||||
|
||||
const title = computed(() => {
|
||||
const v = props.version || {}
|
||||
const ver = v.versionNo || ''
|
||||
const eff = v.effectiveTime || ''
|
||||
const base = '版本价格变更记录'
|
||||
if (ver || eff) return base + ' · ' + (ver || '') + (eff ? ' (' + eff + ')' : '')
|
||||
return base
|
||||
})
|
||||
|
||||
/** Minute key: yyyy-MM-dd HH:mm */
|
||||
function toMinuteKey(time) {
|
||||
if (!time) return '-'
|
||||
const s = String(time).trim()
|
||||
return s.length >= 16 ? s.slice(0, 16) : s
|
||||
}
|
||||
|
||||
const timeTabs = computed(() => {
|
||||
const map = new Map()
|
||||
for (const g of allGroups.value || []) {
|
||||
const key = toMinuteKey(g && g.updateTime)
|
||||
if (!map.has(key)) {
|
||||
map.set(key, { key, label: key, changeCount: 0, productSet: new Set() })
|
||||
}
|
||||
const tab = map.get(key)
|
||||
tab.changeCount += Number(g.changeCount) || 0
|
||||
if (g.productCode) tab.productSet.add(g.productCode)
|
||||
}
|
||||
return Array.from(map.values())
|
||||
.sort((a, b) => String(b.key).localeCompare(String(a.key)))
|
||||
.map(t => ({
|
||||
key: t.key,
|
||||
label: t.key + ' (' + t.productSet.size + ')',
|
||||
changeCount: t.changeCount
|
||||
}))
|
||||
})
|
||||
|
||||
const productRows = computed(() => {
|
||||
const minute = activeTime.value
|
||||
if (!minute) return []
|
||||
const kw = (query.productKeyword || '').trim().toLowerCase()
|
||||
const map = new Map()
|
||||
for (const g of allGroups.value || []) {
|
||||
if (toMinuteKey(g && g.updateTime) !== minute) continue
|
||||
const code = g.productCode || '-'
|
||||
if (kw && !String(code).toLowerCase().includes(kw)) continue
|
||||
let row = map.get(code)
|
||||
if (!row) {
|
||||
row = {
|
||||
productCode: code,
|
||||
action: g.action,
|
||||
operator: g.operator,
|
||||
status: g.status,
|
||||
changeCount: 0,
|
||||
updateTimePrefix: minute
|
||||
}
|
||||
map.set(code, row)
|
||||
}
|
||||
row.changeCount += Number(g.changeCount) || 0
|
||||
if (g.status === 'FAIL') row.status = 'FAIL'
|
||||
if (!row.operator && g.operator) row.operator = g.operator
|
||||
if (!row.action && g.action) row.action = g.action
|
||||
}
|
||||
return Array.from(map.values()).sort((a, b) => String(a.productCode).localeCompare(String(b.productCode)))
|
||||
})
|
||||
|
||||
function close() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
query.productKeyword = ''
|
||||
reload()
|
||||
}
|
||||
|
||||
function onTimeTabChange() {
|
||||
groupOpen.value = false
|
||||
detailOpen.value = false
|
||||
}
|
||||
|
||||
function parseReq(row) {
|
||||
if (!row || !row.requestJson) return {}
|
||||
try {
|
||||
return typeof row.requestJson === 'object' ? row.requestJson : JSON.parse(row.requestJson)
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function actionLabelOf(action) {
|
||||
const found = actionOptions.find(i => i.value === action)
|
||||
return found ? found.label : (action || '-')
|
||||
}
|
||||
|
||||
function actionLabel(row) {
|
||||
return actionLabelOf(parseReq(row).action)
|
||||
}
|
||||
|
||||
function operatorOf(row) {
|
||||
return parseReq(row).operator || '-'
|
||||
}
|
||||
|
||||
function productCodeOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
return keys.productCode || '-'
|
||||
}
|
||||
|
||||
function countryOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
if (keys.countryCode && keys.countryName) return keys.countryName + ' (' + keys.countryCode + ')'
|
||||
return keys.countryCode || keys.countryName || '-'
|
||||
}
|
||||
|
||||
function weightBandOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
if (keys.weightLabel) return String(keys.weightLabel)
|
||||
if (keys.weightMin != null || keys.weightMax != null) {
|
||||
const min = keys.weightMin != null ? String(keys.weightMin) : ''
|
||||
const max = keys.weightMax != null ? String(keys.weightMax) : ''
|
||||
return min + '<W≤' + max
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
function zoneOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
if (keys.zoneCode && keys.zoneName) return keys.zoneName + ' (' + keys.zoneCode + ')'
|
||||
return keys.zoneCode || keys.zoneName || '-'
|
||||
}
|
||||
|
||||
function countryKeyOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
return String(keys.countryCode || keys.countryName || '').toUpperCase()
|
||||
}
|
||||
|
||||
function zoneKeyOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
return String(keys.zoneCode || keys.zoneName || '')
|
||||
}
|
||||
|
||||
function weightKeyOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
if (keys.weightLabel) return String(keys.weightLabel)
|
||||
return String(keys.weightMin ?? '') + '~' + String(keys.weightMax ?? '')
|
||||
}
|
||||
|
||||
const displayGroupRows = computed(() => {
|
||||
const list = (groupRows.value || []).slice()
|
||||
list.sort((a, b) => {
|
||||
const ca = countryKeyOf(a)
|
||||
const cb = countryKeyOf(b)
|
||||
if (ca !== cb) return ca.localeCompare(cb)
|
||||
const za = zoneKeyOf(a)
|
||||
const zb = zoneKeyOf(b)
|
||||
if (za !== zb) return za.localeCompare(zb)
|
||||
return weightKeyOf(a).localeCompare(weightKeyOf(b))
|
||||
})
|
||||
return list
|
||||
})
|
||||
|
||||
function buildSpans(rows, keyOf) {
|
||||
const spans = new Array(rows.length).fill(1)
|
||||
let i = 0
|
||||
while (i < rows.length) {
|
||||
let j = i + 1
|
||||
const key = keyOf(rows[i])
|
||||
while (j < rows.length && keyOf(rows[j]) === key) j++
|
||||
spans[i] = j - i
|
||||
for (let k = i + 1; k < j; k++) spans[k] = 0
|
||||
i = j
|
||||
}
|
||||
return spans
|
||||
}
|
||||
|
||||
const countrySpans = computed(() => buildSpans(displayGroupRows.value, countryKeyOf))
|
||||
|
||||
function spanOf(spans, rowIndex) {
|
||||
const rowspan = spans[rowIndex] || 0
|
||||
return rowspan > 0 ? { rowspan, colspan: 1 } : { rowspan: 0, colspan: 0 }
|
||||
}
|
||||
|
||||
function groupSpanMethod({ columnIndex, rowIndex }) {
|
||||
if (columnIndex === 0) return spanOf(countrySpans.value, rowIndex)
|
||||
return { rowspan: 1, colspan: 1 }
|
||||
}
|
||||
|
||||
function changesOf(row) {
|
||||
const req = parseReq(row)
|
||||
return Array.isArray(req.changes) ? req.changes : []
|
||||
}
|
||||
|
||||
function changeSummary(row) {
|
||||
const list = changesOf(row)
|
||||
const band = weightBandOf(row)
|
||||
const pricePart = !list.length
|
||||
? '-'
|
||||
: list.map(c => {
|
||||
const label = (c && c.fieldLabel) || (c && c.field) || ''
|
||||
const oldV = c && c.oldValue != null ? c.oldValue : ''
|
||||
const newV = c && c.newValue != null ? c.newValue : ''
|
||||
return label + ': ' + oldV + ' -> ' + newV
|
||||
}).join(';')
|
||||
if (band && band !== '-') {
|
||||
return '公斤段 ' + band + (pricePart === '-' ? '' : ':' + pricePart)
|
||||
}
|
||||
return pricePart
|
||||
}
|
||||
|
||||
function statusLabel(status) {
|
||||
const found = statusOptions.find(item => item.value === status)
|
||||
if (found) return found.label
|
||||
return status === 'FAIL' ? '失败' : '成功'
|
||||
}
|
||||
|
||||
function statusTag(status) {
|
||||
return status === 'FAIL' ? 'danger' : 'success'
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
const psvId = props.version && props.version.psvId
|
||||
if (psvId == null) {
|
||||
allGroups.value = []
|
||||
activeTime.value = ''
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listPriceLogGroups({
|
||||
pageNum: 1,
|
||||
pageSize: 500,
|
||||
psvId,
|
||||
priceKind: 'SALES'
|
||||
})
|
||||
allGroups.value = res.rows || []
|
||||
const tabs = timeTabs.value
|
||||
if (!tabs.length) {
|
||||
activeTime.value = ''
|
||||
} else if (!tabs.some(t => t.key === activeTime.value)) {
|
||||
activeTime.value = tabs[0].key
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openProduct(row) {
|
||||
if (!row) return
|
||||
activeProduct.value = row
|
||||
groupTitle.value = '变更明细 · ' + (row.productCode || '-') + (activeTime.value ? ' @ ' + activeTime.value : '')
|
||||
groupQuery.pageNum = 1
|
||||
groupQuery.countryKeyword = ''
|
||||
groupOpen.value = true
|
||||
reloadGroup()
|
||||
}
|
||||
|
||||
function searchGroup() {
|
||||
groupQuery.pageNum = 1
|
||||
reloadGroup()
|
||||
}
|
||||
|
||||
function resetGroupQuery() {
|
||||
groupQuery.countryKeyword = ''
|
||||
groupQuery.pageNum = 1
|
||||
reloadGroup()
|
||||
}
|
||||
|
||||
async function reloadGroup() {
|
||||
const psvId = props.version && props.version.psvId
|
||||
const p = activeProduct.value
|
||||
if (psvId == null || !p) {
|
||||
groupRows.value = []
|
||||
groupTotal.value = 0
|
||||
return
|
||||
}
|
||||
groupLoading.value = true
|
||||
try {
|
||||
const kw = (groupQuery.countryKeyword || '').trim()
|
||||
const res = await listPriceLog({
|
||||
pageNum: groupQuery.pageNum,
|
||||
pageSize: groupQuery.pageSize,
|
||||
psvId,
|
||||
priceKind: 'SALES',
|
||||
productCode: p.productCode === '-' ? undefined : p.productCode,
|
||||
updateTimePrefix: activeTime.value || p.updateTimePrefix || undefined,
|
||||
countryKeyword: kw || undefined
|
||||
})
|
||||
groupRows.value = res.rows || []
|
||||
groupTotal.value = Number(res.total) || 0
|
||||
} finally {
|
||||
groupLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleView(row) {
|
||||
detailTitle.value = '价格变更 #' + (row.bizId || row.docId)
|
||||
detailOpen.value = true
|
||||
detailLoading.value = true
|
||||
detail.value = null
|
||||
getPriceLog(row.docId).then(res => {
|
||||
detail.value = res.data || row
|
||||
}).finally(() => {
|
||||
detailLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.version && props.version.psvId],
|
||||
([open]) => {
|
||||
if (open) {
|
||||
activeTime.value = ''
|
||||
reload()
|
||||
} else {
|
||||
groupOpen.value = false
|
||||
detailOpen.value = false
|
||||
detail.value = null
|
||||
activeProduct.value = null
|
||||
allGroups.value = []
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.price-log-drawer { min-height: 240px; }
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.time-tabs {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.time-tabs :deep(.el-tabs__header) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.change-panel {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid #e8ecf0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.change-panel__head {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
background: #fafbfc;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
</style>
|
||||
@@ -12,13 +12,16 @@
|
||||
</template>
|
||||
<el-tabs v-model="statusTab" class="ver-tabs">
|
||||
<el-tab-pane :label="tabLabel('all')" name="all" />
|
||||
<el-tab-pane :label="tabLabel('0')" name="0" />
|
||||
<el-tab-pane :label="tabLabel('3')" name="3" />
|
||||
<el-tab-pane :label="tabLabel('pending')" name="pending" />
|
||||
<el-tab-pane :label="tabLabel('1')" name="1" />
|
||||
<el-tab-pane :label="tabLabel('2')" name="2" />
|
||||
<el-tab-pane :label="tabLabel('del')" name="del" />
|
||||
</el-tabs>
|
||||
|
||||
<el-table :data="filteredVersions" border empty-text="暂无版本">
|
||||
<el-table-column :label="colVer" width="100" align="center">
|
||||
<template #default="{ row }">{{ row.versionNo || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colEff" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetails(row)">{{ row.effectiveTime || '未设置' }}</el-button>
|
||||
@@ -29,38 +32,47 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="colStatus" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="versionStatusTag(row.status)" size="small">{{ versionStatusLabel(row.status) }}</el-tag>
|
||||
<el-tag :type="isDeletedVersion(row) ? 'danger' : versionStatusTag(row.status)" size="small">{{ statusText(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colCreate" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ row.createTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colRows" width="100" align="center">
|
||||
<template #default="{ row }">{{ detailCount(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colAction" width="460" align="center" fixed="right">
|
||||
<el-table-column :label="colAction" width="540" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetails(row)">查看详情</el-button>
|
||||
<el-button link type="primary" @click="emit('view-import-attach', row)">查看附件</el-button>
|
||||
<el-button
|
||||
v-if="showEditButton(row)"
|
||||
link
|
||||
type="primary"
|
||||
v-hasPermi="['monitor:priceLog:list']"
|
||||
@click="emit('view-price-log', row)"
|
||||
>变更记录</el-button>
|
||||
<el-button
|
||||
v-if="!isDeletedVersion(row) && showEditButton(row)"
|
||||
link
|
||||
type="primary"
|
||||
@click="emit('edit', row)"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
v-if="isDraftVersion(row)"
|
||||
v-if="!isDeletedVersion(row) && isDraftVersion(row)"
|
||||
link
|
||||
type="success"
|
||||
@click="emit('activate', row)"
|
||||
v-hasPermi="editPerm"
|
||||
>启动</el-button>
|
||||
<el-button
|
||||
v-if="isPendingVersion(row)"
|
||||
v-if="!isDeletedVersion(row) && isPendingVersion(row)"
|
||||
link
|
||||
type="primary"
|
||||
@click="emit('reschedule', row)"
|
||||
v-hasPermi="editPerm"
|
||||
>修改生效时间</el-button>
|
||||
<el-button
|
||||
v-if="isDraftVersion(row) || isPendingVersion(row) || isEffectVersion(row)"
|
||||
v-if="!isDeletedVersion(row) && (isDraftVersion(row) || isPendingVersion(row) || isEffectVersion(row))"
|
||||
link
|
||||
type="danger"
|
||||
:loading="isRemoving(row)"
|
||||
@@ -89,9 +101,11 @@ const props = defineProps({
|
||||
modelValue: { type: [Number, String], default: undefined },
|
||||
removingPsvId: { type: [Number, String], default: null }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'create-version', 'open-details', 'view-import-attach', 'activate', 'reschedule', 'edit', 'remove'])
|
||||
const emit = defineEmits(['update:modelValue', 'create-version', 'open-details', 'view-import-attach', 'view-price-log', 'activate', 'reschedule', 'edit', 'remove'])
|
||||
|
||||
const title = '套餐版本'
|
||||
const colVer = '版本号'
|
||||
const colCreate = '创建时间'
|
||||
const colEff = '生效时间'
|
||||
const colExp = '失效时间'
|
||||
const colStatus = '状态'
|
||||
@@ -99,28 +113,42 @@ const colRows = '明细行数'
|
||||
const colAction = '操作'
|
||||
const statusTab = ref('all')
|
||||
|
||||
function isDeletedVersion(row) {
|
||||
return !!row && String(row.isDel) === '1'
|
||||
}
|
||||
|
||||
function statusText(row) {
|
||||
if (isDeletedVersion(row)) return '已删除'
|
||||
return versionStatusLabel(row.status)
|
||||
}
|
||||
|
||||
const activeVersions = computed(() => (props.versions || []).filter(v => !isDeletedVersion(v)))
|
||||
const deletedVersions = computed(() => (props.versions || []).filter(v => isDeletedVersion(v)))
|
||||
|
||||
const counts = computed(() => {
|
||||
const map = { all: 0, '0': 0, '3': 0, '1': 0, '2': 0 }
|
||||
;(props.versions || []).forEach(v => {
|
||||
const map = { all: 0, pending: 0, '1': 0, '2': 0, del: 0 }
|
||||
activeVersions.value.forEach(v => {
|
||||
map.all++
|
||||
const s = String(v.status == null ? '' : v.status)
|
||||
if (map[s] != null) map[s]++
|
||||
if (s === '0' || s === '3') map.pending++
|
||||
else if (map[s] != null) map[s]++
|
||||
})
|
||||
map.del = deletedVersions.value.length
|
||||
return map
|
||||
})
|
||||
|
||||
function tabLabel(key) {
|
||||
const labels = {
|
||||
all: '全部',
|
||||
'0': '草稿',
|
||||
'3': '待生效',
|
||||
pending: '待生效',
|
||||
'1': '已生效',
|
||||
'2': '已失效'
|
||||
'2': '已失效',
|
||||
del: '已删除'
|
||||
}
|
||||
return labels[key] + ' (' + (counts.value[key] || 0) + ')'
|
||||
}
|
||||
|
||||
const hasDraft = computed(() => (props.versions || []).some(v => isDraftVersion(v)))
|
||||
const hasDraft = computed(() => activeVersions.value.some(v => isDraftVersion(v)))
|
||||
|
||||
function toEffMs(v) {
|
||||
if (!v || !v.effectiveTime) return NaN
|
||||
@@ -156,10 +184,21 @@ function sortVersions(list) {
|
||||
}
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
const list = props.versions || []
|
||||
const filtered = statusTab.value === 'all'
|
||||
? list
|
||||
: list.filter(v => String(v.status) === statusTab.value)
|
||||
if (statusTab.value === 'del') {
|
||||
return sortVersions(deletedVersions.value)
|
||||
}
|
||||
const list = activeVersions.value
|
||||
let filtered
|
||||
if (statusTab.value === 'all') {
|
||||
filtered = list
|
||||
} else if (statusTab.value === 'pending') {
|
||||
filtered = list.filter(v => {
|
||||
const s = String(v.status)
|
||||
return s === '0' || s === '3'
|
||||
})
|
||||
} else {
|
||||
filtered = list.filter(v => String(v.status) === statusTab.value)
|
||||
}
|
||||
return sortVersions(filtered)
|
||||
})
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
@create-version="onCreateVersion"
|
||||
@open-details="onOpenVersionDetails"
|
||||
@view-import-attach="onViewImportAttach"
|
||||
@view-price-log="onViewPriceLog"
|
||||
@activate="onActivateVersion"
|
||||
@reschedule="onRescheduleVersion"
|
||||
@edit="onEditVersion"
|
||||
@@ -39,6 +40,10 @@
|
||||
:set-type="set.setType"
|
||||
/>
|
||||
<set-version-excel-file-drawer ref="versionExcelDrawerRef" />
|
||||
<set-version-price-log-drawer
|
||||
v-model="priceLogVisible"
|
||||
:version="priceLogVersion"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="activateVisible"
|
||||
@@ -81,6 +86,7 @@ import SetCustomerTable from './components/detail/SetCustomerTable.vue'
|
||||
import SetVersionTable from './components/detail/SetVersionTable.vue'
|
||||
import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.vue'
|
||||
import SetVersionExcelFileDrawer from './components/detail/SetVersionExcelFileDrawer.vue'
|
||||
import SetVersionPriceLogDrawer from './components/detail/SetVersionPriceLogDrawer.vue'
|
||||
import PriceEditPanel from './components/create/PriceEditPanel.vue'
|
||||
import { usePriceSetRefData } from './composables/usePriceSetRefData'
|
||||
import { usePriceSetModule } from './composables/usePriceSetModule'
|
||||
@@ -102,6 +108,8 @@ const detailVersion = ref(null)
|
||||
const versionExcelDrawerRef = ref()
|
||||
const editVisible = ref(false)
|
||||
const editPsvId = ref(undefined)
|
||||
const priceLogVisible = ref(false)
|
||||
const priceLogVersion = ref(null)
|
||||
/** 已加载的套餐ID;页面被 keep-alive 缓存复用时据此判断是否需要重新加载 */
|
||||
const loadedId = ref(null)
|
||||
/** 从详情页跳到新建/编辑套餐版本页后,返回时套餐版本数据可能已变化,需强制刷新 */
|
||||
@@ -115,6 +123,8 @@ function resetTransientState() {
|
||||
detailVersion.value = null
|
||||
editVisible.value = false
|
||||
editPsvId.value = undefined
|
||||
priceLogVisible.value = false
|
||||
priceLogVersion.value = null
|
||||
activateVisible.value = false
|
||||
activateSubmitting.value = false
|
||||
activateMode.value = 'activate'
|
||||
@@ -134,6 +144,13 @@ function onViewImportAttach(row) {
|
||||
versionExcelDrawerRef.value?.open?.(row)
|
||||
}
|
||||
|
||||
function onViewPriceLog(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
priceLogVersion.value = row
|
||||
priceLogVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
function load() {
|
||||
const id = route.query.priceSetId
|
||||
if (!id) return
|
||||
@@ -282,6 +299,7 @@ function findPrevVersionEffectiveTime(row) {
|
||||
let prev = null
|
||||
for (const v of versions) {
|
||||
if (!v || v.psvId == null || String(v.psvId) === String(row.psvId)) continue
|
||||
if (String(v.isDel) === '1') continue
|
||||
const vs = Number(v.versionSeq) || 0
|
||||
if (vs >= curSeq || !v.effectiveTime) continue
|
||||
if (!prev || vs > (Number(prev.versionSeq) || 0)) prev = v
|
||||
@@ -295,6 +313,7 @@ function collectActivateBoundaryTimes(excludePsvId) {
|
||||
const seen = new Set()
|
||||
for (const v of set.value.versions || []) {
|
||||
if (!v || v.psvId == null || String(v.psvId) === String(excludePsvId)) continue
|
||||
if (String(v.isDel) === '1') continue
|
||||
;[v.effectiveTime, v.expireTime].forEach(t => {
|
||||
const n = t ? normalizeToHour(t) : ''
|
||||
if (!n || seen.has(n)) return
|
||||
@@ -432,7 +451,7 @@ function submitActivate() {
|
||||
function onCreateVersion() {
|
||||
const priceSetId = set.value.priceSetId
|
||||
if (priceSetId == null) return
|
||||
const hasDraft = (set.value.versions || []).some(v => v && String(v.status) === '0')
|
||||
const hasDraft = (set.value.versions || []).some(v => v && String(v.isDel) !== '1' && String(v.status) === '0')
|
||||
if (hasDraft) {
|
||||
proxy.$modal.msgWarning('已有草稿版本,请先启动或删除后再新建')
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user