增加查看价格套餐历史版本的变更
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.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("查询成功");
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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