增加查看价格套餐历史版本的变更

This commit is contained in:
wuweihua
2026-11-05 18:28:57 +08:00
committed by caojia
parent 220b01127b
commit f57af555ea
11 changed files with 956 additions and 6 deletions
@@ -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;
}
}
@@ -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);