增加价格变更日志

This commit is contained in:
wuweihua
2026-11-06 10:11:15 +08:00
parent 1090d838a6
commit e415843b14
12 changed files with 487 additions and 58 deletions
@@ -37,13 +37,14 @@ public class PriceLogController extends BaseController
@RequestParam(value = "priceKind", required = false) String priceKind, @RequestParam(value = "priceKind", required = false) String priceKind,
@RequestParam(value = "productCode", required = false) String productCode, @RequestParam(value = "productCode", required = false) String productCode,
@RequestParam(value = "updateTime", required = false) String updateTime, @RequestParam(value = "updateTime", required = false) String updateTime,
@RequestParam(value = "updateTimePrefix", required = false) String updateTimePrefix) @RequestParam(value = "updateTimePrefix", required = false) String updateTimePrefix,
@RequestParam(value = "countryKeyword", required = false) String countryKeyword)
{ {
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, psvId, priceKind, BillSearchPage page = billSearchStore.search(EsLogType.PRICE_CHANGE, bizId, status, psvId, priceKind,
productCode, updateTime, updateTimePrefix, pageNum, pageSize); productCode, updateTime, updateTimePrefix, countryKeyword, pageNum, pageSize);
TableDataInfo rsp = new TableDataInfo(); TableDataInfo rsp = new TableDataInfo();
rsp.setCode(HttpStatus.SUCCESS); rsp.setCode(HttpStatus.SUCCESS);
rsp.setMsg("查询成功"); rsp.setMsg("查询成功");
@@ -450,6 +450,11 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
realChanges.add(c); realChanges.add(c);
} }
} }
// Rebind/update with no field diff: skip empty UPDATE noise (full package save upsert)
if (existing != null && realChanges.isEmpty())
{
return;
}
saveLog(bind.getCustomerId(), customer == null ? null : customer.getCustomerCode(), saveLog(bind.getCustomerId(), customer == null ? null : customer.getCustomerCode(),
customer == null ? null : customer.getCustomerName(), customer == null ? null : customer.getCustomerName(),
BmsCustomerLog.MODULE_PRICE_SET, newSet.getPriceSetName(), BmsCustomerLog.MODULE_PRICE_SET, newSet.getPriceSetName(),
@@ -44,6 +44,9 @@ public interface PrcPriceSetDetailMapper
public int countByPsvId(Long psvId); public int countByPsvId(Long psvId);
/** Count detail rows including soft-deleted (for deleted-version tab). */
public int countByPsvIdAll(Long psvId);
public int copyDetailsFromVersion(@Param("srcPsvId") Long srcPsvId, @Param("newPsvId") Long newPsvId); public int copyDetailsFromVersion(@Param("srcPsvId") Long srcPsvId, @Param("newPsvId") Long newPsvId);
/** Copy version effective time, expire time and status onto catalog detail rows. */ /** Copy version effective time, expire time and status onto catalog detail rows. */
@@ -173,6 +173,15 @@ public class EsBillSearchStore implements IBillSearchStore
@Override @Override
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind, public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix, int pageNum, int pageSize) String productCode, String updateTime, String updateTimePrefix, int pageNum, int pageSize)
{
return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, updateTimePrefix,
null, pageNum, pageSize);
}
@Override
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix, String countryKeyword,
int pageNum, int pageSize)
{ {
BillSearchPage page = new BillSearchPage(); BillSearchPage page = new BillSearchPage();
if (pageNum < 1) if (pageNum < 1)
@@ -197,7 +206,7 @@ public class EsBillSearchStore implements IBillSearchStore
sort.add(sortItem); sort.add(sortItem);
body.put("sort", sort); body.put("sort", sort);
body.put("query", buildQuery(logType, bizId, status, psvId, priceKind, productCode, updateTime, body.put("query", buildQuery(logType, bizId, status, psvId, priceKind, productCode, updateTime,
updateTimePrefix)); updateTimePrefix, countryKeyword));
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);
@@ -378,7 +387,7 @@ public class EsBillSearchStore implements IBillSearchStore
} }
private JSONObject buildQuery(String logType, String bizId, String status, Long psvId, String priceKind, private JSONObject buildQuery(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix) String productCode, String updateTime, String updateTimePrefix, String countryKeyword)
{ {
JSONArray filters = new JSONArray(); JSONArray filters = new JSONArray();
addTerm(filters, "logType", logType); addTerm(filters, "logType", logType);
@@ -387,6 +396,7 @@ public class EsBillSearchStore implements IBillSearchStore
addPsvIdFilter(filters, psvId); addPsvIdFilter(filters, psvId);
addTerm(filters, "request.priceKind", priceKind); addTerm(filters, "request.priceKind", priceKind);
addProductCodeFilter(filters, productCode); addProductCodeFilter(filters, productCode);
addCountryKeywordFilter(filters, countryKeyword);
addUpdateTimeFilter(filters, updateTime); addUpdateTimeFilter(filters, updateTime);
addUpdateTimePrefixFilter(filters, updateTimePrefix); addUpdateTimePrefixFilter(filters, updateTimePrefix);
if (filters.isEmpty()) if (filters.isEmpty())
@@ -430,6 +440,65 @@ public class EsBillSearchStore implements IBillSearchStore
filters.add(boolWrap); filters.add(boolWrap);
} }
/** Match country code / name (exact or contains). */
private void addCountryKeywordFilter(JSONArray filters, String countryKeyword)
{
if (StringUtils.isEmpty(countryKeyword))
{
return;
}
String kw = countryKeyword.trim();
String wild = "*" + escapeWildcard(kw) + "*";
JSONArray should = new JSONArray();
addShouldTerm(should, "request.bizKeys.countryCode.keyword", kw);
addShouldTerm(should, "request.bizKeys.countryCode", kw);
addShouldWildcard(should, "request.bizKeys.countryCode.keyword", wild);
addShouldWildcard(should, "request.bizKeys.countryCode", wild);
addShouldWildcard(should, "request.bizKeys.countryName.keyword", wild);
addShouldWildcard(should, "request.bizKeys.countryName", wild);
JSONObject matchName = new JSONObject();
JSONObject matchField = new JSONObject();
matchField.put("query", kw);
matchName.put("request.bizKeys.countryName", matchField);
JSONObject matchWrap = new JSONObject();
matchWrap.put("match", matchName);
should.add(matchWrap);
JSONObject inner = new JSONObject();
inner.put("should", should);
inner.put("minimum_should_match", Integer.valueOf(1));
JSONObject boolWrap = new JSONObject();
boolWrap.put("bool", inner);
filters.add(boolWrap);
}
private void addShouldTerm(JSONArray should, String field, String value)
{
JSONObject term = new JSONObject();
term.put(field, value);
JSONObject wrap = new JSONObject();
wrap.put("term", term);
should.add(wrap);
}
private void addShouldWildcard(JSONArray should, String field, String value)
{
JSONObject wild = new JSONObject();
wild.put(field, value);
JSONObject wrap = new JSONObject();
wrap.put("wildcard", wild);
should.add(wrap);
}
private static String escapeWildcard(String raw)
{
if (raw == null)
{
return "";
}
return raw.replace("\\", "\\\\").replace("*", "\\*").replace("?", "\\?");
}
private void addUpdateTimeFilter(JSONArray filters, String updateTime) private void addUpdateTimeFilter(JSONArray filters, String updateTime)
{ {
if (StringUtils.isEmpty(updateTime)) if (StringUtils.isEmpty(updateTime))
@@ -38,6 +38,17 @@ public interface IBillSearchStore
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind, default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix, int pageNum, int pageSize) String productCode, String updateTime, String updateTimePrefix, int pageNum, int pageSize)
{
return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, updateTimePrefix,
null, pageNum, pageSize);
}
/**
* Search with optional countryKeyword (matches bizKeys.countryCode / countryName).
*/
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix, String countryKeyword,
int pageNum, int pageSize)
{ {
return search(logType, bizId, status, pageNum, pageSize); return search(logType, bizId, status, pageNum, pageSize);
} }
@@ -152,11 +152,31 @@ public final class PriceChangeEsLog
keys.put("countryCode", d.getCountryCode()); keys.put("countryCode", d.getCountryCode());
keys.put("countryName", d.getCountryName()); keys.put("countryName", d.getCountryName());
keys.put("zoneCode", d.getZoneCode()); keys.put("zoneCode", d.getZoneCode());
keys.put("zoneName", d.getZoneName());
keys.put("weightMin", d.getWeightMin()); keys.put("weightMin", d.getWeightMin());
keys.put("weightMax", d.getWeightMax()); keys.put("weightMax", d.getWeightMax());
keys.put("weightLabel", weightBandOf(d));
return keys; return keys;
} }
/** Display weight band for sales detail logs (e.g. 0&lt;W≤0.5). */
public static String weightBandOf(PrcPriceSetDetail d)
{
if (d == null)
{
return "";
}
if (StringUtils.isNotEmpty(d.getWeightLabel()))
{
return d.getWeightLabel();
}
if (d.getWeightMin() == null && d.getWeightMax() == null)
{
return "";
}
return stringify(d.getWeightMin()) + "<W\u2264" + stringify(d.getWeightMax());
}
public static void save(IBillSearchStore store, String bizId, String priceKind, String action, public static void save(IBillSearchStore store, String bizId, String priceKind, String action,
Map<String, Object> bizKeys, List<Map<String, String>> changes) Map<String, Object> bizKeys, List<Map<String, String>> changes)
{ {
@@ -53,6 +53,11 @@ import com.bms.price.search.PriceChangeEsLog;
import com.bms.price.service.IPrcPriceExcelFileService; import com.bms.price.service.IPrcPriceExcelFileService;
import com.bms.price.service.IPrcPriceSetService; import com.bms.price.service.IPrcPriceSetService;
import com.bms.price.task.PrcPriceSetSaveAsyncRunner; import com.bms.price.task.PrcPriceSetSaveAsyncRunner;
import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.domain.BmsCustomerPriceSet;
import com.bms.customer.customerinfo.domain.BmsPriceSetOption;
import com.bms.customer.customerinfo.service.IBmsCustomerLogService;
import com.bms.customer.customerinfo.service.IBmsCustomerService;
/** /**
* 价格套餐 服务层实现 * 价格套餐 服务层实现
@@ -130,6 +135,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
@Autowired @Autowired
private IBillSearchStore billSearchStore; private IBillSearchStore billSearchStore;
@Autowired
private IBmsCustomerLogService customerLogService;
@Autowired
private IBmsCustomerService bmsCustomerService;
@Override @Override
public PrcPriceSet selectPrcPriceSetById(Long priceSetId) public PrcPriceSet selectPrcPriceSetById(Long priceSetId)
{ {
@@ -2511,7 +2522,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
} }
try try
{ {
return customerMapper.insertPrcCustomerPriceSet(customer); int rows = customerMapper.insertPrcCustomerPriceSet(customer);
if (rows > 0)
{
logCustomerPriceSetBind(null, customer, db);
}
return rows;
} }
catch (Exception e) catch (Exception e)
{ {
@@ -2610,6 +2626,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
} }
validateCustomer(row, db); validateCustomer(row, db);
customerMapper.updatePrcCustomerPriceSet(row); customerMapper.updatePrcCustomerPriceSet(row);
logCustomerPriceSetBind(existing, row, db);
replaced++; replaced++;
} }
else else
@@ -2630,6 +2647,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
} }
throw e; throw e;
} }
logCustomerPriceSetBind(null, row, db);
inserted++; inserted++;
} }
} }
@@ -2657,12 +2675,30 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
assertChildEditable(db); assertChildEditable(db);
customer.setPriceSetId(old.getPriceSetId()); customer.setPriceSetId(old.getPriceSetId());
validateCustomer(customer, db); validateCustomer(customer, db);
return customerMapper.updatePrcCustomerPriceSet(customer); int rows = customerMapper.updatePrcCustomerPriceSet(customer);
if (rows > 0)
{
if (customer.getCustomerId() == null)
{
customer.setCustomerId(old.getCustomerId());
}
if (StringUtils.isEmpty(customer.getCustomerCode()))
{
customer.setCustomerCode(old.getCustomerCode());
}
if (StringUtils.isEmpty(customer.getCustomerName()))
{
customer.setCustomerName(old.getCustomerName());
}
logCustomerPriceSetBind(old, customer, db);
}
return rows;
} }
@Override @Override
public int deleteCustomerByIds(Long[] customerPricesetIds) public int deleteCustomerByIds(Long[] customerPricesetIds)
{ {
List<PrcCustomerPriceSet> snapshots = new ArrayList<PrcCustomerPriceSet>();
for (Long id : customerPricesetIds) for (Long id : customerPricesetIds)
{ {
PrcCustomerPriceSet old = customerMapper.selectPrcCustomerPriceSetById(id); PrcCustomerPriceSet old = customerMapper.selectPrcCustomerPriceSetById(id);
@@ -2675,8 +2711,17 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{ {
throw new ServiceException(MessageUtils.message("price.set.customer.unbindAudit")); throw new ServiceException(MessageUtils.message("price.set.customer.unbindAudit"));
} }
snapshots.add(old);
} }
return customerMapper.deletePrcCustomerPriceSetByIds(customerPricesetIds); int rows = customerMapper.deletePrcCustomerPriceSetByIds(customerPricesetIds);
if (rows > 0)
{
for (PrcCustomerPriceSet snapshot : snapshots)
{
logCustomerPriceSetUnbind(snapshot);
}
}
return rows;
} }
private void replaceChildren(PrcPriceSet set) private void replaceChildren(PrcPriceSet set)
@@ -2747,6 +2792,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
c.setUpdateBy(set.getUpdateBy()); c.setUpdateBy(set.getUpdateBy());
customerMapper.updatePrcCustomerPriceSet(c); customerMapper.updatePrcCustomerPriceSet(c);
keepIds.add(hit.getCustomerPricesetId()); keepIds.add(hit.getCustomerPricesetId());
logCustomerPriceSetBind(hit, c, set);
} }
else else
{ {
@@ -2757,6 +2803,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{ {
keepIds.add(c.getCustomerPricesetId()); keepIds.add(c.getCustomerPricesetId());
} }
logCustomerPriceSetBind(null, c, set);
} }
} }
catch (Exception e) catch (Exception e)
@@ -2778,10 +2825,99 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
} }
if (!orphanIds.isEmpty()) if (!orphanIds.isEmpty())
{ {
for (Long orphanId : orphanIds)
{
PrcCustomerPriceSet snapshot = byId.get(orphanId);
if (snapshot != null)
{
logCustomerPriceSetUnbind(snapshot);
}
}
customerMapper.deletePrcCustomerPriceSetByIds(orphanIds.toArray(new Long[0])); customerMapper.deletePrcCustomerPriceSetByIds(orphanIds.toArray(new Long[0]));
} }
} }
/** Write customer biz log for package bind / rebind (visible in customer info Logs). */
private void logCustomerPriceSetBind(PrcCustomerPriceSet existing, PrcCustomerPriceSet bind, PrcPriceSet set)
{
if (customerLogService == null || bind == null || set == null)
{
return;
}
BmsCustomer customer = resolveCustomerForLog(bind);
if (customer == null && bind.getCustomerId() != null)
{
customer = new BmsCustomer();
customer.setCustomerId(bind.getCustomerId());
customer.setCustomerCode(bind.getCustomerCode());
customer.setCustomerName(bind.getCustomerName());
}
BmsCustomerPriceSet existingLog = toBmsCustomerPriceSet(existing, resolvePriceSetName(existing));
BmsCustomerPriceSet bindLog = toBmsCustomerPriceSet(bind, set.getPriceSetName());
if (bindLog.getCustomerId() == null && customer != null)
{
bindLog.setCustomerId(customer.getCustomerId());
}
BmsPriceSetOption option = new BmsPriceSetOption();
option.setPriceSetId(set.getPriceSetId());
option.setPriceSetCode(set.getPriceSetCode());
option.setPriceSetName(set.getPriceSetName());
customerLogService.recordPriceSetBind(customer, existingLog, bindLog, option);
}
private void logCustomerPriceSetUnbind(PrcCustomerPriceSet bind)
{
if (customerLogService == null || bind == null)
{
return;
}
BmsCustomerPriceSet snapshot = toBmsCustomerPriceSet(bind, resolvePriceSetName(bind));
customerLogService.recordPriceSetUnbind(snapshot);
}
private BmsCustomer resolveCustomerForLog(PrcCustomerPriceSet bind)
{
if (bind == null || bmsCustomerService == null)
{
return null;
}
if (bind.getCustomerId() != null)
{
return bmsCustomerService.selectBmsCustomerByCustomerId(bind.getCustomerId());
}
return null;
}
private String resolvePriceSetName(PrcCustomerPriceSet bind)
{
if (bind == null || bind.getPriceSetId() == null)
{
return null;
}
PrcPriceSet set = setMapper.selectPrcPriceSetById(bind.getPriceSetId());
return set == null ? null : set.getPriceSetName();
}
private BmsCustomerPriceSet toBmsCustomerPriceSet(PrcCustomerPriceSet src, String priceSetName)
{
if (src == null)
{
return null;
}
BmsCustomerPriceSet dest = new BmsCustomerPriceSet();
dest.setCustomerPricesetId(src.getCustomerPricesetId());
dest.setPriceSetId(src.getPriceSetId());
dest.setCustomerId(src.getCustomerId());
dest.setCustomerCode(src.getCustomerCode());
dest.setCustomerName(src.getCustomerName());
dest.setBindPriority(src.getBindPriority());
dest.setPrBegindate(src.getPrBegindate());
dest.setPrEnddate(src.getPrEnddate());
dest.setPriceSetNote(src.getPriceSetNote());
dest.setPriceSetName(priceSetName);
return dest;
}
private void reportSaveProgress(PrcPriceSet set, int percent, String message) private void reportSaveProgress(PrcPriceSet set, int percent, String message)
{ {
if (set == null || StringUtils.isEmpty(set.getSaveTaskId()) || saveAsyncRunner == null) if (set == null || StringUtils.isEmpty(set.getSaveTaskId()) || saveAsyncRunner == null)
@@ -3482,6 +3618,22 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{ {
fillVersionChildren(version); fillVersionChildren(version);
} }
// Soft-deleted EFFECT versions for the "deleted" tab (status may still be EFFECT)
PrcPriceSetVersion dq = new PrcPriceSetVersion();
dq.setPriceSetId(priceSetId);
dq.setPriceType(com.bms.price.domain.PriceType.SALES);
dq.setIsDel("1");
List<PrcPriceSetVersion> deleted = versionMapper.selectPrcPriceSetVersionList(dq);
if (deleted != null)
{
for (PrcPriceSetVersion version : deleted)
{
version.setProducts(new ArrayList<PrcPriceSetProduct>());
version.setDetails(new ArrayList<PrcPriceSetDetail>());
version.setDetailCount(Integer.valueOf(detailMapper.countByPsvIdAll(version.getPsvId())));
versions.add(version);
}
}
set.setVersions(versions); set.setVersions(versions);
PrcPriceSetVersion current = pickCurrentVersion(versions); PrcPriceSetVersion current = pickCurrentVersion(versions);
set.setVersion(current); set.setVersion(current);
@@ -3508,14 +3660,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{ {
return null; return null;
} }
PrcPriceSetVersion firstAlive = null;
for (PrcPriceSetVersion v : versions) for (PrcPriceSetVersion v : versions)
{ {
if (v == null || "1".equals(v.getIsDel()))
{
continue;
}
if (firstAlive == null)
{
firstAlive = v;
}
if (VER_EFFECT.equals(v.getStatus())) if (VER_EFFECT.equals(v.getStatus()))
{ {
return v; return v;
} }
} }
return versions.get(0); return firstAlive;
} }
private void saveOrUpdateVersion(PrcPriceSet set) private void saveOrUpdateVersion(PrcPriceSet set)
@@ -158,6 +158,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select count(1) from prc_price_set_detail where psv_id = #{psvId} and ifnull(is_del, '0') = '0' select count(1) from prc_price_set_detail where psv_id = #{psvId} and ifnull(is_del, '0') = '0'
</select> </select>
<select id="countByPsvIdAll" parameterType="Long" resultType="int">
select count(1) from prc_price_set_detail where psv_id = #{psvId}
</select>
<select id="selectDetailOptionRows" resultMap="PrcPriceSetDetailResult"> <select id="selectDetailOptionRows" resultMap="PrcPriceSetDetailResult">
select distinct product_code, product_name, country_code, country_name, zone_code, zone_name select distinct product_code, product_name, country_code, country_name, zone_code, zone_name
from prc_price_set_detail from prc_price_set_detail
@@ -37,7 +37,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectPrcPriceSetVersionList" parameterType="PrcPriceSetVersion" resultMap="PrcPriceSetVersionResult"> <select id="selectPrcPriceSetVersionList" parameterType="PrcPriceSetVersion" resultMap="PrcPriceSetVersionResult">
<include refid="selectPrcPriceSetVersionVo"/> <include refid="selectPrcPriceSetVersionVo"/>
<where> <where>
and ifnull(is_del, '0') = '0' <choose>
<when test="isDel != null and isDel != ''">
and ifnull(is_del, '0') = #{isDel}
</when>
<otherwise>
and ifnull(is_del, '0') = '0'
</otherwise>
</choose>
<if test="priceSetId != null"> <if test="priceSetId != null">
AND price_set_id = #{priceSetId} AND price_set_id = #{priceSetId}
</if> </if>
@@ -9,9 +9,13 @@
> >
<div class="price-log-drawer" v-loading="loading"> <div class="price-log-drawer" v-loading="loading">
<div class="toolbar"> <div class="toolbar">
<el-select v-model="query.status" clearable :placeholder="phStatus" style="width: 120px" @change="reload"> <el-input
<el-option v-for="o in statusOptions" :key="o.value" :label="o.label" :value="o.value" /> v-model="query.productKeyword"
</el-select> clearable
:placeholder="phProduct"
style="width: 220px"
@keyup.enter="reload"
/>
<el-button type="primary" @click="reload">查询</el-button> <el-button type="primary" @click="reload">查询</el-button>
<el-button @click="resetQuery">重置</el-button> <el-button @click="resetQuery">重置</el-button>
</div> </div>
@@ -36,11 +40,6 @@
<template #default="{ row }">{{ row.operator || '-' }}</template> <template #default="{ row }">{{ row.operator || '-' }}</template>
</el-table-column> </el-table-column>
<el-table-column :label="colCount" width="100" align="center" prop="changeCount" /> <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"> <el-table-column :label="colOps" width="90" align="center" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="openProduct(row)">明细</el-button> <el-button link type="primary" @click="openProduct(row)">明细</el-button>
@@ -50,23 +49,41 @@
</template> </template>
</div> </div>
<el-drawer v-model="groupOpen" size="780px" :title="groupTitle" append-to-body destroy-on-close> <el-drawer v-model="groupOpen" size="860px" :title="groupTitle" append-to-body destroy-on-close>
<div v-loading="groupLoading"> <div v-loading="groupLoading">
<el-table :data="groupRows" border height="calc(100vh - 220px)" :empty-text="emptyGroup"> <div class="toolbar">
<el-input
v-model="groupQuery.countryKeyword"
clearable
:placeholder="phCountry"
style="width: 220px"
@keyup.enter="searchGroup"
/>
<el-button type="primary" @click="searchGroup">查询</el-button>
<el-button @click="resetGroupQuery">重置</el-button>
</div>
<el-table
:data="displayGroupRows"
border
height="calc(100vh - 260px)"
:empty-text="emptyGroup"
:span-method="groupSpanMethod"
>
<el-table-column :label="colCountry" min-width="140" show-overflow-tooltip> <el-table-column :label="colCountry" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ countryOf(row) }}</template> <template #default="{ row }">{{ countryOf(row) }}</template>
</el-table-column> </el-table-column>
<el-table-column :label="colZone" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ zoneOf(row) }}</template>
</el-table-column>
<el-table-column :label="colWeight" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ weightBandOf(row) }}</template>
</el-table-column>
<el-table-column :label="colAction" width="90" align="center"> <el-table-column :label="colAction" width="90" align="center">
<template #default="{ row }">{{ actionLabel(row) }}</template> <template #default="{ row }">{{ actionLabel(row) }}</template>
</el-table-column> </el-table-column>
<el-table-column :label="colChangeSummary" min-width="220" show-overflow-tooltip> <el-table-column :label="colChangeSummary" min-width="240" show-overflow-tooltip>
<template #default="{ row }">{{ changeSummary(row) }}</template> <template #default="{ row }">{{ changeSummary(row) }}</template>
</el-table-column> </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="colTime" width="170" align="center" prop="updateTime" />
<el-table-column :label="colOps" width="90" align="center" fixed="right"> <el-table-column :label="colOps" width="90" align="center" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
@@ -101,6 +118,8 @@
<el-descriptions-item :label="lbTime">{{ detail.updateTime || '-' }}</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="lbProduct">{{ productCodeOf(detail) }}</el-descriptions-item>
<el-descriptions-item :label="lbCountry">{{ countryOf(detail) }}</el-descriptions-item> <el-descriptions-item :label="lbCountry">{{ countryOf(detail) }}</el-descriptions-item>
<el-descriptions-item :label="lbWeight">{{ weightBandOf(detail) }}</el-descriptions-item>
<el-descriptions-item :label="lbZone">{{ zoneOf(detail) }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
<div class="change-panel"> <div class="change-panel">
@@ -133,7 +152,7 @@ const visible = computed({
const loading = ref(false) const loading = ref(false)
const allGroups = ref([]) const allGroups = ref([])
const query = reactive({ status: undefined }) const query = reactive({ productKeyword: '' })
const activeTime = ref('') const activeTime = ref('')
const groupOpen = ref(false) const groupOpen = ref(false)
@@ -142,14 +161,15 @@ const groupRows = ref([])
const groupTotal = ref(0) const groupTotal = ref(0)
const groupTitle = ref('') const groupTitle = ref('')
const activeProduct = ref(null) const activeProduct = ref(null)
const groupQuery = reactive({ pageNum: 1, pageSize: 20 }) const groupQuery = reactive({ pageNum: 1, pageSize: 20, countryKeyword: '' })
const detailOpen = ref(false) const detailOpen = ref(false)
const detailLoading = ref(false) const detailLoading = ref(false)
const detail = ref(null) const detail = ref(null)
const detailTitle = ref('') const detailTitle = ref('')
const phStatus = '状态' const phProduct = '产品代码'
const phCountry = '国家代码/名称'
const emptyText = '该版本暂无价格变更记录' const emptyText = '该版本暂无价格变更记录'
const emptyProducts = '该时间点暂无产品变更' const emptyProducts = '该时间点暂无产品变更'
const emptyGroup = '该产品暂无明细变更' const emptyGroup = '该产品暂无明细变更'
@@ -159,6 +179,8 @@ const colAction = '操作'
const colOperator = '操作人' const colOperator = '操作人'
const colProduct = '产品代码' const colProduct = '产品代码'
const colCountry = '国家' const colCountry = '国家'
const colZone = '分区'
const colWeight = '公斤段'
const colCount = '变更条数' const colCount = '变更条数'
const colChangeSummary = '变更摘要' const colChangeSummary = '变更摘要'
const colStatus = '状态' const colStatus = '状态'
@@ -173,6 +195,8 @@ const lbStatus = '状态'
const lbTime = '写入时间' const lbTime = '写入时间'
const lbProduct = '产品代码' const lbProduct = '产品代码'
const lbCountry = '国家' const lbCountry = '国家'
const lbWeight = '公斤段'
const lbZone = '分区'
const statusOptions = [ const statusOptions = [
{ label: '成功', value: 'SUCCESS' }, { label: '成功', value: 'SUCCESS' },
@@ -224,10 +248,12 @@ const timeTabs = computed(() => {
const productRows = computed(() => { const productRows = computed(() => {
const minute = activeTime.value const minute = activeTime.value
if (!minute) return [] if (!minute) return []
const kw = (query.productKeyword || '').trim().toLowerCase()
const map = new Map() const map = new Map()
for (const g of allGroups.value || []) { for (const g of allGroups.value || []) {
if (toMinuteKey(g && g.updateTime) !== minute) continue if (toMinuteKey(g && g.updateTime) !== minute) continue
const code = g.productCode || '-' const code = g.productCode || '-'
if (kw && !String(code).toLowerCase().includes(kw)) continue
let row = map.get(code) let row = map.get(code)
if (!row) { if (!row) {
row = { row = {
@@ -253,7 +279,7 @@ function close() {
} }
function resetQuery() { function resetQuery() {
query.status = undefined query.productKeyword = ''
reload() reload()
} }
@@ -295,6 +321,79 @@ function countryOf(row) {
return keys.countryCode || keys.countryName || '-' return keys.countryCode || keys.countryName || '-'
} }
function weightBandOf(row) {
const keys = parseReq(row).bizKeys || {}
if (keys.weightLabel) return String(keys.weightLabel)
if (keys.weightMin != null || keys.weightMax != null) {
const min = keys.weightMin != null ? String(keys.weightMin) : ''
const max = keys.weightMax != null ? String(keys.weightMax) : ''
return min + '<W≤' + max
}
return '-'
}
function zoneOf(row) {
const keys = parseReq(row).bizKeys || {}
if (keys.zoneCode && keys.zoneName) return keys.zoneName + ' (' + keys.zoneCode + ')'
return keys.zoneCode || keys.zoneName || '-'
}
function countryKeyOf(row) {
const keys = parseReq(row).bizKeys || {}
return String(keys.countryCode || keys.countryName || '').toUpperCase()
}
function zoneKeyOf(row) {
const keys = parseReq(row).bizKeys || {}
return String(keys.zoneCode || keys.zoneName || '')
}
function weightKeyOf(row) {
const keys = parseReq(row).bizKeys || {}
if (keys.weightLabel) return String(keys.weightLabel)
return String(keys.weightMin ?? '') + '~' + String(keys.weightMax ?? '')
}
const displayGroupRows = computed(() => {
const list = (groupRows.value || []).slice()
list.sort((a, b) => {
const ca = countryKeyOf(a)
const cb = countryKeyOf(b)
if (ca !== cb) return ca.localeCompare(cb)
const za = zoneKeyOf(a)
const zb = zoneKeyOf(b)
if (za !== zb) return za.localeCompare(zb)
return weightKeyOf(a).localeCompare(weightKeyOf(b))
})
return list
})
function buildSpans(rows, keyOf) {
const spans = new Array(rows.length).fill(1)
let i = 0
while (i < rows.length) {
let j = i + 1
const key = keyOf(rows[i])
while (j < rows.length && keyOf(rows[j]) === key) j++
spans[i] = j - i
for (let k = i + 1; k < j; k++) spans[k] = 0
i = j
}
return spans
}
const countrySpans = computed(() => buildSpans(displayGroupRows.value, countryKeyOf))
function spanOf(spans, rowIndex) {
const rowspan = spans[rowIndex] || 0
return rowspan > 0 ? { rowspan, colspan: 1 } : { rowspan: 0, colspan: 0 }
}
function groupSpanMethod({ columnIndex, rowIndex }) {
if (columnIndex === 0) return spanOf(countrySpans.value, rowIndex)
return { rowspan: 1, colspan: 1 }
}
function changesOf(row) { function changesOf(row) {
const req = parseReq(row) const req = parseReq(row)
return Array.isArray(req.changes) ? req.changes : [] return Array.isArray(req.changes) ? req.changes : []
@@ -302,13 +401,19 @@ function changesOf(row) {
function changeSummary(row) { function changeSummary(row) {
const list = changesOf(row) const list = changesOf(row)
if (!list.length) return '-' const band = weightBandOf(row)
return list.map(c => { const pricePart = !list.length
const label = (c && c.fieldLabel) || (c && c.field) || '' ? '-'
const oldV = c && c.oldValue != null ? c.oldValue : '' : list.map(c => {
const newV = c && c.newValue != null ? c.newValue : '' const label = (c && c.fieldLabel) || (c && c.field) || ''
return label + ': ' + oldV + ' -> ' + newV const oldV = c && c.oldValue != null ? c.oldValue : ''
}).join(';') const newV = c && c.newValue != null ? c.newValue : ''
return label + ': ' + oldV + ' -> ' + newV
}).join(';')
if (band && band !== '-') {
return '公斤段 ' + band + (pricePart === '-' ? '' : ':' + pricePart)
}
return pricePart
} }
function statusLabel(status) { function statusLabel(status) {
@@ -333,7 +438,6 @@ async function reload() {
const res = await listPriceLogGroups({ const res = await listPriceLogGroups({
pageNum: 1, pageNum: 1,
pageSize: 500, pageSize: 500,
status: query.status,
psvId, psvId,
priceKind: 'SALES' priceKind: 'SALES'
}) })
@@ -354,10 +458,22 @@ function openProduct(row) {
activeProduct.value = row activeProduct.value = row
groupTitle.value = '变更明细 · ' + (row.productCode || '-') + (activeTime.value ? ' @ ' + activeTime.value : '') groupTitle.value = '变更明细 · ' + (row.productCode || '-') + (activeTime.value ? ' @ ' + activeTime.value : '')
groupQuery.pageNum = 1 groupQuery.pageNum = 1
groupQuery.countryKeyword = ''
groupOpen.value = true groupOpen.value = true
reloadGroup() reloadGroup()
} }
function searchGroup() {
groupQuery.pageNum = 1
reloadGroup()
}
function resetGroupQuery() {
groupQuery.countryKeyword = ''
groupQuery.pageNum = 1
reloadGroup()
}
async function reloadGroup() { async function reloadGroup() {
const psvId = props.version && props.version.psvId const psvId = props.version && props.version.psvId
const p = activeProduct.value const p = activeProduct.value
@@ -368,14 +484,15 @@ async function reloadGroup() {
} }
groupLoading.value = true groupLoading.value = true
try { try {
const kw = (groupQuery.countryKeyword || '').trim()
const res = await listPriceLog({ const res = await listPriceLog({
pageNum: groupQuery.pageNum, pageNum: groupQuery.pageNum,
pageSize: groupQuery.pageSize, pageSize: groupQuery.pageSize,
status: query.status,
psvId, psvId,
priceKind: 'SALES', priceKind: 'SALES',
productCode: p.productCode === '-' ? undefined : p.productCode, productCode: p.productCode === '-' ? undefined : p.productCode,
updateTimePrefix: activeTime.value || p.updateTimePrefix || undefined updateTimePrefix: activeTime.value || p.updateTimePrefix || undefined,
countryKeyword: kw || undefined
}) })
groupRows.value = res.rows || [] groupRows.value = res.rows || []
groupTotal.value = Number(res.total) || 0 groupTotal.value = Number(res.total) || 0
@@ -12,10 +12,10 @@
</template> </template>
<el-tabs v-model="statusTab" class="ver-tabs"> <el-tabs v-model="statusTab" class="ver-tabs">
<el-tab-pane :label="tabLabel('all')" name="all" /> <el-tab-pane :label="tabLabel('all')" name="all" />
<el-tab-pane :label="tabLabel('0')" name="0" /> <el-tab-pane :label="tabLabel('pending')" name="pending" />
<el-tab-pane :label="tabLabel('3')" name="3" />
<el-tab-pane :label="tabLabel('1')" name="1" /> <el-tab-pane :label="tabLabel('1')" name="1" />
<el-tab-pane :label="tabLabel('2')" name="2" /> <el-tab-pane :label="tabLabel('2')" name="2" />
<el-tab-pane :label="tabLabel('del')" name="del" />
</el-tabs> </el-tabs>
<el-table :data="filteredVersions" border empty-text="暂无版本"> <el-table :data="filteredVersions" border empty-text="暂无版本">
@@ -32,9 +32,12 @@
</el-table-column> </el-table-column>
<el-table-column :label="colStatus" width="100" align="center"> <el-table-column :label="colStatus" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="versionStatusTag(row.status)" size="small">{{ versionStatusLabel(row.status) }}</el-tag> <el-tag :type="isDeletedVersion(row) ? 'danger' : versionStatusTag(row.status)" size="small">{{ statusText(row) }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="colCreate" min-width="170" align="center">
<template #default="{ row }">{{ row.createTime || '-' }}</template>
</el-table-column>
<el-table-column :label="colRows" width="100" align="center"> <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>
@@ -49,27 +52,27 @@
@click="emit('view-price-log', row)" @click="emit('view-price-log', row)"
>变更记录</el-button> >变更记录</el-button>
<el-button <el-button
v-if="showEditButton(row)" v-if="!isDeletedVersion(row) && showEditButton(row)"
link link
type="primary" type="primary"
@click="emit('edit', row)" @click="emit('edit', row)"
>编辑</el-button> >编辑</el-button>
<el-button <el-button
v-if="isDraftVersion(row)" v-if="!isDeletedVersion(row) && isDraftVersion(row)"
link link
type="success" type="success"
@click="emit('activate', row)" @click="emit('activate', row)"
v-hasPermi="editPerm" v-hasPermi="editPerm"
>启动</el-button> >启动</el-button>
<el-button <el-button
v-if="isPendingVersion(row)" v-if="!isDeletedVersion(row) && isPendingVersion(row)"
link link
type="primary" type="primary"
@click="emit('reschedule', row)" @click="emit('reschedule', row)"
v-hasPermi="editPerm" v-hasPermi="editPerm"
>修改生效时间</el-button> >修改生效时间</el-button>
<el-button <el-button
v-if="isDraftVersion(row) || isPendingVersion(row) || isEffectVersion(row)" v-if="!isDeletedVersion(row) && (isDraftVersion(row) || isPendingVersion(row) || isEffectVersion(row))"
link link
type="danger" type="danger"
:loading="isRemoving(row)" :loading="isRemoving(row)"
@@ -102,6 +105,7 @@ const emit = defineEmits(['update:modelValue', 'create-version', 'open-details',
const title = '套餐版本' const title = '套餐版本'
const colVer = '版本号' const colVer = '版本号'
const colCreate = '创建时间'
const colEff = '生效时间' const colEff = '生效时间'
const colExp = '失效时间' const colExp = '失效时间'
const colStatus = '状态' const colStatus = '状态'
@@ -109,28 +113,42 @@ const colRows = '明细行数'
const colAction = '操作' const colAction = '操作'
const statusTab = ref('all') const statusTab = ref('all')
function isDeletedVersion(row) {
return !!row && String(row.isDel) === '1'
}
function statusText(row) {
if (isDeletedVersion(row)) return '已删除'
return versionStatusLabel(row.status)
}
const activeVersions = computed(() => (props.versions || []).filter(v => !isDeletedVersion(v)))
const deletedVersions = computed(() => (props.versions || []).filter(v => isDeletedVersion(v)))
const counts = computed(() => { const counts = computed(() => {
const map = { all: 0, '0': 0, '3': 0, '1': 0, '2': 0 } const map = { all: 0, pending: 0, '1': 0, '2': 0, del: 0 }
;(props.versions || []).forEach(v => { activeVersions.value.forEach(v => {
map.all++ map.all++
const s = String(v.status == null ? '' : v.status) const s = String(v.status == null ? '' : v.status)
if (map[s] != null) map[s]++ if (s === '0' || s === '3') map.pending++
else if (map[s] != null) map[s]++
}) })
map.del = deletedVersions.value.length
return map return map
}) })
function tabLabel(key) { function tabLabel(key) {
const labels = { const labels = {
all: '全部', all: '全部',
'0': '草稿', pending: '待生效',
'3': '待生效',
'1': '已生效', '1': '已生效',
'2': '已失效' '2': '已失效',
del: '已删除'
} }
return labels[key] + ' (' + (counts.value[key] || 0) + ')' return labels[key] + ' (' + (counts.value[key] || 0) + ')'
} }
const hasDraft = computed(() => (props.versions || []).some(v => isDraftVersion(v))) const hasDraft = computed(() => activeVersions.value.some(v => isDraftVersion(v)))
function toEffMs(v) { function toEffMs(v) {
if (!v || !v.effectiveTime) return NaN if (!v || !v.effectiveTime) return NaN
@@ -166,10 +184,21 @@ function sortVersions(list) {
} }
const filteredVersions = computed(() => { const filteredVersions = computed(() => {
const list = props.versions || [] if (statusTab.value === 'del') {
const filtered = statusTab.value === 'all' return sortVersions(deletedVersions.value)
? list }
: list.filter(v => String(v.status) === statusTab.value) const list = activeVersions.value
let filtered
if (statusTab.value === 'all') {
filtered = list
} else if (statusTab.value === 'pending') {
filtered = list.filter(v => {
const s = String(v.status)
return s === '0' || s === '3'
})
} else {
filtered = list.filter(v => String(v.status) === statusTab.value)
}
return sortVersions(filtered) return sortVersions(filtered)
}) })
+3 -1
View File
@@ -299,6 +299,7 @@ function findPrevVersionEffectiveTime(row) {
let prev = null let prev = null
for (const v of versions) { for (const v of versions) {
if (!v || v.psvId == null || String(v.psvId) === String(row.psvId)) continue if (!v || v.psvId == null || String(v.psvId) === String(row.psvId)) continue
if (String(v.isDel) === '1') continue
const vs = Number(v.versionSeq) || 0 const vs = Number(v.versionSeq) || 0
if (vs >= curSeq || !v.effectiveTime) continue if (vs >= curSeq || !v.effectiveTime) continue
if (!prev || vs > (Number(prev.versionSeq) || 0)) prev = v if (!prev || vs > (Number(prev.versionSeq) || 0)) prev = v
@@ -312,6 +313,7 @@ function collectActivateBoundaryTimes(excludePsvId) {
const seen = new Set() const seen = new Set()
for (const v of set.value.versions || []) { for (const v of set.value.versions || []) {
if (!v || v.psvId == null || String(v.psvId) === String(excludePsvId)) continue if (!v || v.psvId == null || String(v.psvId) === String(excludePsvId)) continue
if (String(v.isDel) === '1') continue
;[v.effectiveTime, v.expireTime].forEach(t => { ;[v.effectiveTime, v.expireTime].forEach(t => {
const n = t ? normalizeToHour(t) : '' const n = t ? normalizeToHour(t) : ''
if (!n || seen.has(n)) return if (!n || seen.has(n)) return
@@ -449,7 +451,7 @@ function submitActivate() {
function onCreateVersion() { function onCreateVersion() {
const priceSetId = set.value.priceSetId const priceSetId = set.value.priceSetId
if (priceSetId == null) return if (priceSetId == null) return
const hasDraft = (set.value.versions || []).some(v => v && String(v.status) === '0') const hasDraft = (set.value.versions || []).some(v => v && String(v.isDel) !== '1' && String(v.status) === '0')
if (hasDraft) { if (hasDraft) {
proxy.$modal.msgWarning('已有草稿版本,请先启动或删除后再新建') proxy.$modal.msgWarning('已有草稿版本,请先启动或删除后再新建')
return return