Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ced4f237f | |||
| 87c75922aa |
+30
-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,39 @@ 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)
|
||||
{
|
||||
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, 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.
|
||||
*/
|
||||
|
||||
@@ -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,27 @@ 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)
|
||||
{
|
||||
BillSearchPage page = new BillSearchPage();
|
||||
if (pageNum < 1)
|
||||
@@ -171,7 +196,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));
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<String> entity = new HttpEntity<String>(body.toJSONString(), headers);
|
||||
@@ -217,6 +243,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 +377,18 @@ 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)
|
||||
{
|
||||
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);
|
||||
addUpdateTimeFilter(filters, updateTime);
|
||||
addUpdateTimePrefixFilter(filters, updateTimePrefix);
|
||||
if (filters.isEmpty())
|
||||
{
|
||||
JSONObject matchAll = new JSONObject();
|
||||
@@ -268,6 +402,136 @@ 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);
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
@@ -17,4 +17,37 @@ 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, 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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -1582,6 +1582,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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
@@ -184,12 +194,18 @@
|
||||
: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>
|
||||
|
||||
<script setup>
|
||||
import AddIncomeFeeDialog from './AddIncomeFeeDialog.vue'
|
||||
import EditChargeWeightDialog from './EditChargeWeightDialog.vue'
|
||||
import BillBillingHistoryDrawer from './BillBillingHistoryDrawer.vue'
|
||||
import { deleteIncomeBillFee } from '@/api/price/billing'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
@@ -205,6 +221,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)
|
||||
@@ -224,6 +241,7 @@ const payFees = computed(() => {
|
||||
|
||||
watch(() => props.detail, () => {
|
||||
feeTab.value = 'income'
|
||||
historyOpen.value = false
|
||||
})
|
||||
|
||||
function canEditManualFee(row) {
|
||||
@@ -465,6 +483,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);
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
<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-select v-model="query.status" clearable :placeholder="phStatus" style="width: 120px" @change="reload">
|
||||
<el-option v-for="o in statusOptions" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
<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="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="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="780px" :title="groupTitle" append-to-body destroy-on-close>
|
||||
<div v-loading="groupLoading">
|
||||
<el-table :data="groupRows" border height="calc(100vh - 220px)" :empty-text="emptyGroup">
|
||||
<el-table-column :label="colCountry" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ countryOf(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="220" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ changeSummary(row) }}</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="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>
|
||||
|
||||
<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({ status: undefined })
|
||||
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 })
|
||||
|
||||
const detailOpen = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detail = ref(null)
|
||||
const detailTitle = ref('')
|
||||
|
||||
const phStatus = '状态'
|
||||
const emptyText = '该版本暂无价格变更记录'
|
||||
const emptyProducts = '该时间点暂无产品变更'
|
||||
const emptyGroup = '该产品暂无明细变更'
|
||||
const emptyDetail = '暂无详情'
|
||||
const emptyChanges = '无字段变更'
|
||||
const colAction = '操作'
|
||||
const colOperator = '操作人'
|
||||
const colProduct = '产品代码'
|
||||
const colCountry = '国家'
|
||||
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 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 map = new Map()
|
||||
for (const g of allGroups.value || []) {
|
||||
if (toMinuteKey(g && g.updateTime) !== minute) continue
|
||||
const code = g.productCode || '-'
|
||||
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.status = undefined
|
||||
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 changesOf(row) {
|
||||
const req = parseReq(row)
|
||||
return Array.isArray(req.changes) ? req.changes : []
|
||||
}
|
||||
|
||||
function changeSummary(row) {
|
||||
const list = changesOf(row)
|
||||
if (!list.length) return '-'
|
||||
return 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(';')
|
||||
}
|
||||
|
||||
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,
|
||||
status: query.status,
|
||||
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
|
||||
groupOpen.value = true
|
||||
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 res = await listPriceLog({
|
||||
pageNum: groupQuery.pageNum,
|
||||
pageSize: groupQuery.pageSize,
|
||||
status: query.status,
|
||||
psvId,
|
||||
priceKind: 'SALES',
|
||||
productCode: p.productCode === '-' ? undefined : p.productCode,
|
||||
updateTimePrefix: activeTime.value || p.updateTimePrefix || 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>
|
||||
@@ -35,10 +35,16 @@
|
||||
<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
|
||||
link
|
||||
type="primary"
|
||||
v-hasPermi="['monitor:priceLog:list']"
|
||||
@click="emit('view-price-log', row)"
|
||||
>变更记录</el-button>
|
||||
<el-button
|
||||
v-if="showEditButton(row)"
|
||||
link
|
||||
@@ -89,7 +95,7 @@ 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 colEff = '生效时间'
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user