增加查看价格套餐历史版本的变更
This commit is contained in:
+30
-2
@@ -16,6 +16,7 @@ import com.bms.common.core.page.TableSupport;
|
|||||||
import com.bms.price.search.BillSearchPage;
|
import com.bms.price.search.BillSearchPage;
|
||||||
import com.bms.price.search.EsLogType;
|
import com.bms.price.search.EsLogType;
|
||||||
import com.bms.price.search.IBillSearchStore;
|
import com.bms.price.search.IBillSearchStore;
|
||||||
|
import com.bms.price.search.PriceChangeGroupPage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 来自 ES 的价格变更前后日志(销售价 / 产品价 / 红线价)。
|
* 来自 ES 的价格变更前后日志(销售价 / 产品价 / 红线价)。
|
||||||
@@ -31,12 +32,39 @@ public class PriceLogController extends BaseController
|
|||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public TableDataInfo list(
|
public TableDataInfo list(
|
||||||
@RequestParam(value = "bizId", required = false) String bizId,
|
@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();
|
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||||
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
|
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
|
||||||
int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().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();
|
TableDataInfo rsp = new TableDataInfo();
|
||||||
rsp.setCode(HttpStatus.SUCCESS);
|
rsp.setCode(HttpStatus.SUCCESS);
|
||||||
rsp.setMsg("查询成功");
|
rsp.setMsg("查询成功");
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ package com.bms.price.search;
|
|||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpEntity;
|
import org.springframework.http.HttpEntity;
|
||||||
@@ -148,6 +152,27 @@ public class EsBillSearchStore implements IBillSearchStore
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BillSearchPage search(String logType, String bizId, String status, int pageNum, int pageSize)
|
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();
|
BillSearchPage page = new BillSearchPage();
|
||||||
if (pageNum < 1)
|
if (pageNum < 1)
|
||||||
@@ -171,7 +196,8 @@ public class EsBillSearchStore implements IBillSearchStore
|
|||||||
sortItem.put("updatedAt.keyword", sortOpt);
|
sortItem.put("updatedAt.keyword", sortOpt);
|
||||||
sort.add(sortItem);
|
sort.add(sortItem);
|
||||||
body.put("sort", sort);
|
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();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
HttpEntity<String> entity = new HttpEntity<String>(body.toJSONString(), headers);
|
HttpEntity<String> entity = new HttpEntity<String>(body.toJSONString(), headers);
|
||||||
@@ -217,6 +243,108 @@ public class EsBillSearchStore implements IBillSearchStore
|
|||||||
return page;
|
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)
|
private BillSyncSnapshot toSnapshot(JSONObject source, String id)
|
||||||
{
|
{
|
||||||
if (source == null)
|
if (source == null)
|
||||||
@@ -249,12 +377,18 @@ public class EsBillSearchStore implements IBillSearchStore
|
|||||||
return snap;
|
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();
|
JSONArray filters = new JSONArray();
|
||||||
addTerm(filters, "logType", logType);
|
addTerm(filters, "logType", logType);
|
||||||
addWildcard(filters, "bizId", bizId);
|
addWildcard(filters, "bizId", bizId);
|
||||||
addTerm(filters, "status", status);
|
addTerm(filters, "status", status);
|
||||||
|
addPsvIdFilter(filters, psvId);
|
||||||
|
addTerm(filters, "request.priceKind", priceKind);
|
||||||
|
addProductCodeFilter(filters, productCode);
|
||||||
|
addUpdateTimeFilter(filters, updateTime);
|
||||||
|
addUpdateTimePrefixFilter(filters, updateTimePrefix);
|
||||||
if (filters.isEmpty())
|
if (filters.isEmpty())
|
||||||
{
|
{
|
||||||
JSONObject matchAll = new JSONObject();
|
JSONObject matchAll = new JSONObject();
|
||||||
@@ -268,6 +402,136 @@ public class EsBillSearchStore implements IBillSearchStore
|
|||||||
return query;
|
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)
|
private void addTerm(JSONArray filters, String field, String value)
|
||||||
{
|
{
|
||||||
if (StringUtils.isEmpty(value))
|
if (StringUtils.isEmpty(value))
|
||||||
|
|||||||
@@ -17,4 +17,37 @@ public interface IBillSearchStore
|
|||||||
BillSyncSnapshot getByDocId(String docId);
|
BillSyncSnapshot getByDocId(String docId);
|
||||||
|
|
||||||
BillSearchPage search(String logType, String bizId, String status, int pageNum, int pageSize);
|
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_DELETE = "DELETE";
|
||||||
public static final String ACTION_IMPORT = "IMPORT";
|
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()
|
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()
|
public static String operator()
|
||||||
{
|
{
|
||||||
|
String override = OPERATOR_OVERRIDE.get();
|
||||||
|
if (StringUtils.isNotEmpty(override))
|
||||||
|
{
|
||||||
|
return override;
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
String name = SecurityUtils.getUsername();
|
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)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
|
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
|
||||||
String taskId, boolean updateOnly, boolean replaceByProductCountry, String countryCode) throws Exception
|
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);
|
PrcPriceSet db = requireSet(priceSetId);
|
||||||
assertChildEditable(db);
|
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) {
|
export function getPriceLog(docId) {
|
||||||
return request({
|
return request({
|
||||||
url: '/monitor/priceLog/' + encodeURIComponent(docId),
|
url: '/monitor/priceLog/' + encodeURIComponent(docId),
|
||||||
|
|||||||
@@ -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">
|
<el-table-column :label="colRows" width="100" align="center">
|
||||||
<template #default="{ row }">{{ detailCount(row) }}</template>
|
<template #default="{ row }">{{ detailCount(row) }}</template>
|
||||||
</el-table-column>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="openDetails(row)">查看详情</el-button>
|
<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" @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
|
<el-button
|
||||||
v-if="showEditButton(row)"
|
v-if="showEditButton(row)"
|
||||||
link
|
link
|
||||||
@@ -89,7 +95,7 @@ const props = defineProps({
|
|||||||
modelValue: { type: [Number, String], default: undefined },
|
modelValue: { type: [Number, String], default: undefined },
|
||||||
removingPsvId: { type: [Number, String], default: null }
|
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 title = '套餐版本'
|
||||||
const colEff = '生效时间'
|
const colEff = '生效时间'
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
@create-version="onCreateVersion"
|
@create-version="onCreateVersion"
|
||||||
@open-details="onOpenVersionDetails"
|
@open-details="onOpenVersionDetails"
|
||||||
@view-import-attach="onViewImportAttach"
|
@view-import-attach="onViewImportAttach"
|
||||||
|
@view-price-log="onViewPriceLog"
|
||||||
@activate="onActivateVersion"
|
@activate="onActivateVersion"
|
||||||
@reschedule="onRescheduleVersion"
|
@reschedule="onRescheduleVersion"
|
||||||
@edit="onEditVersion"
|
@edit="onEditVersion"
|
||||||
@@ -39,6 +40,10 @@
|
|||||||
:set-type="set.setType"
|
:set-type="set.setType"
|
||||||
/>
|
/>
|
||||||
<set-version-excel-file-drawer ref="versionExcelDrawerRef" />
|
<set-version-excel-file-drawer ref="versionExcelDrawerRef" />
|
||||||
|
<set-version-price-log-drawer
|
||||||
|
v-model="priceLogVisible"
|
||||||
|
:version="priceLogVersion"
|
||||||
|
/>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="activateVisible"
|
v-model="activateVisible"
|
||||||
@@ -81,6 +86,7 @@ import SetCustomerTable from './components/detail/SetCustomerTable.vue'
|
|||||||
import SetVersionTable from './components/detail/SetVersionTable.vue'
|
import SetVersionTable from './components/detail/SetVersionTable.vue'
|
||||||
import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.vue'
|
import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.vue'
|
||||||
import SetVersionExcelFileDrawer from './components/detail/SetVersionExcelFileDrawer.vue'
|
import SetVersionExcelFileDrawer from './components/detail/SetVersionExcelFileDrawer.vue'
|
||||||
|
import SetVersionPriceLogDrawer from './components/detail/SetVersionPriceLogDrawer.vue'
|
||||||
import PriceEditPanel from './components/create/PriceEditPanel.vue'
|
import PriceEditPanel from './components/create/PriceEditPanel.vue'
|
||||||
import { usePriceSetRefData } from './composables/usePriceSetRefData'
|
import { usePriceSetRefData } from './composables/usePriceSetRefData'
|
||||||
import { usePriceSetModule } from './composables/usePriceSetModule'
|
import { usePriceSetModule } from './composables/usePriceSetModule'
|
||||||
@@ -102,6 +108,8 @@ const detailVersion = ref(null)
|
|||||||
const versionExcelDrawerRef = ref()
|
const versionExcelDrawerRef = ref()
|
||||||
const editVisible = ref(false)
|
const editVisible = ref(false)
|
||||||
const editPsvId = ref(undefined)
|
const editPsvId = ref(undefined)
|
||||||
|
const priceLogVisible = ref(false)
|
||||||
|
const priceLogVersion = ref(null)
|
||||||
/** 已加载的套餐ID;页面被 keep-alive 缓存复用时据此判断是否需要重新加载 */
|
/** 已加载的套餐ID;页面被 keep-alive 缓存复用时据此判断是否需要重新加载 */
|
||||||
const loadedId = ref(null)
|
const loadedId = ref(null)
|
||||||
/** 从详情页跳到新建/编辑套餐版本页后,返回时套餐版本数据可能已变化,需强制刷新 */
|
/** 从详情页跳到新建/编辑套餐版本页后,返回时套餐版本数据可能已变化,需强制刷新 */
|
||||||
@@ -115,6 +123,8 @@ function resetTransientState() {
|
|||||||
detailVersion.value = null
|
detailVersion.value = null
|
||||||
editVisible.value = false
|
editVisible.value = false
|
||||||
editPsvId.value = undefined
|
editPsvId.value = undefined
|
||||||
|
priceLogVisible.value = false
|
||||||
|
priceLogVersion.value = null
|
||||||
activateVisible.value = false
|
activateVisible.value = false
|
||||||
activateSubmitting.value = false
|
activateSubmitting.value = false
|
||||||
activateMode.value = 'activate'
|
activateMode.value = 'activate'
|
||||||
@@ -134,6 +144,13 @@ function onViewImportAttach(row) {
|
|||||||
versionExcelDrawerRef.value?.open?.(row)
|
versionExcelDrawerRef.value?.open?.(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onViewPriceLog(row) {
|
||||||
|
if (!row || row.psvId == null) return
|
||||||
|
priceLogVersion.value = row
|
||||||
|
priceLogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
const id = route.query.priceSetId
|
const id = route.query.priceSetId
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|||||||
Reference in New Issue
Block a user