增加价格变更日志

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 = "productCode", required = false) String productCode,
@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();
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, psvId, priceKind,
productCode, updateTime, updateTimePrefix, pageNum, pageSize);
productCode, updateTime, updateTimePrefix, countryKeyword, pageNum, pageSize);
TableDataInfo rsp = new TableDataInfo();
rsp.setCode(HttpStatus.SUCCESS);
rsp.setMsg("查询成功");
@@ -450,6 +450,11 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
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(),
customer == null ? null : customer.getCustomerName(),
BmsCustomerLog.MODULE_PRICE_SET, newSet.getPriceSetName(),
@@ -44,6 +44,9 @@ public interface PrcPriceSetDetailMapper
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);
/** Copy version effective time, expire time and status onto catalog detail rows. */
@@ -173,6 +173,15 @@ public class EsBillSearchStore implements IBillSearchStore
@Override
public 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, 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();
if (pageNum < 1)
@@ -197,7 +206,7 @@ public class EsBillSearchStore implements IBillSearchStore
sort.add(sortItem);
body.put("sort", sort);
body.put("query", buildQuery(logType, bizId, status, psvId, priceKind, productCode, updateTime,
updateTimePrefix));
updateTimePrefix, countryKeyword));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
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,
String productCode, String updateTime, String updateTimePrefix)
String productCode, String updateTime, String updateTimePrefix, String countryKeyword)
{
JSONArray filters = new JSONArray();
addTerm(filters, "logType", logType);
@@ -387,6 +396,7 @@ public class EsBillSearchStore implements IBillSearchStore
addPsvIdFilter(filters, psvId);
addTerm(filters, "request.priceKind", priceKind);
addProductCodeFilter(filters, productCode);
addCountryKeywordFilter(filters, countryKeyword);
addUpdateTimeFilter(filters, updateTime);
addUpdateTimePrefixFilter(filters, updateTimePrefix);
if (filters.isEmpty())
@@ -430,6 +440,65 @@ public class EsBillSearchStore implements IBillSearchStore
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)
{
if (StringUtils.isEmpty(updateTime))
@@ -38,6 +38,17 @@ public interface IBillSearchStore
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, 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);
}
@@ -152,11 +152,31 @@ public final class PriceChangeEsLog
keys.put("countryCode", d.getCountryCode());
keys.put("countryName", d.getCountryName());
keys.put("zoneCode", d.getZoneCode());
keys.put("zoneName", d.getZoneName());
keys.put("weightMin", d.getWeightMin());
keys.put("weightMax", d.getWeightMax());
keys.put("weightLabel", weightBandOf(d));
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,
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.IPrcPriceSetService;
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
private IBillSearchStore billSearchStore;
@Autowired
private IBmsCustomerLogService customerLogService;
@Autowired
private IBmsCustomerService bmsCustomerService;
@Override
public PrcPriceSet selectPrcPriceSetById(Long priceSetId)
{
@@ -2511,7 +2522,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
try
{
return customerMapper.insertPrcCustomerPriceSet(customer);
int rows = customerMapper.insertPrcCustomerPriceSet(customer);
if (rows > 0)
{
logCustomerPriceSetBind(null, customer, db);
}
return rows;
}
catch (Exception e)
{
@@ -2610,6 +2626,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
validateCustomer(row, db);
customerMapper.updatePrcCustomerPriceSet(row);
logCustomerPriceSetBind(existing, row, db);
replaced++;
}
else
@@ -2630,6 +2647,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
throw e;
}
logCustomerPriceSetBind(null, row, db);
inserted++;
}
}
@@ -2657,12 +2675,30 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
assertChildEditable(db);
customer.setPriceSetId(old.getPriceSetId());
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
public int deleteCustomerByIds(Long[] customerPricesetIds)
{
List<PrcCustomerPriceSet> snapshots = new ArrayList<PrcCustomerPriceSet>();
for (Long id : customerPricesetIds)
{
PrcCustomerPriceSet old = customerMapper.selectPrcCustomerPriceSetById(id);
@@ -2675,8 +2711,17 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
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)
@@ -2747,6 +2792,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
c.setUpdateBy(set.getUpdateBy());
customerMapper.updatePrcCustomerPriceSet(c);
keepIds.add(hit.getCustomerPricesetId());
logCustomerPriceSetBind(hit, c, set);
}
else
{
@@ -2757,6 +2803,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
keepIds.add(c.getCustomerPricesetId());
}
logCustomerPriceSetBind(null, c, set);
}
}
catch (Exception e)
@@ -2778,10 +2825,99 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
if (!orphanIds.isEmpty())
{
for (Long orphanId : orphanIds)
{
PrcCustomerPriceSet snapshot = byId.get(orphanId);
if (snapshot != null)
{
logCustomerPriceSetUnbind(snapshot);
}
}
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)
{
if (set == null || StringUtils.isEmpty(set.getSaveTaskId()) || saveAsyncRunner == null)
@@ -3482,6 +3618,22 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
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);
PrcPriceSetVersion current = pickCurrentVersion(versions);
set.setVersion(current);
@@ -3508,14 +3660,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
return null;
}
PrcPriceSetVersion firstAlive = null;
for (PrcPriceSetVersion v : versions)
{
if (v == null || "1".equals(v.getIsDel()))
{
continue;
}
if (firstAlive == null)
{
firstAlive = v;
}
if (VER_EFFECT.equals(v.getStatus()))
{
return v;
}
}
return versions.get(0);
return firstAlive;
}
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>
<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 distinct product_code, product_name, country_code, country_name, zone_code, zone_name
from prc_price_set_detail
@@ -37,7 +37,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectPrcPriceSetVersionList" parameterType="PrcPriceSetVersion" resultMap="PrcPriceSetVersionResult">
<include refid="selectPrcPriceSetVersionVo"/>
<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">
AND price_set_id = #{priceSetId}
</if>
@@ -9,9 +9,13 @@
>
<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-input
v-model="query.productKeyword"
clearable
:placeholder="phProduct"
style="width: 220px"
@keyup.enter="reload"
/>
<el-button type="primary" @click="reload">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
@@ -36,11 +40,6 @@
<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>
@@ -50,23 +49,41 @@
</template>
</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">
<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>
<template #default="{ row }">{{ countryOf(row) }}</template>
</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">
<template #default="{ row }">{{ actionLabel(row) }}</template>
</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>
</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 }">
@@ -101,6 +118,8 @@
<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-item :label="lbWeight">{{ weightBandOf(detail) }}</el-descriptions-item>
<el-descriptions-item :label="lbZone">{{ zoneOf(detail) }}</el-descriptions-item>
</el-descriptions>
<div class="change-panel">
@@ -133,7 +152,7 @@ const visible = computed({
const loading = ref(false)
const allGroups = ref([])
const query = reactive({ status: undefined })
const query = reactive({ productKeyword: '' })
const activeTime = ref('')
const groupOpen = ref(false)
@@ -142,14 +161,15 @@ const groupRows = ref([])
const groupTotal = ref(0)
const groupTitle = ref('')
const activeProduct = ref(null)
const groupQuery = reactive({ pageNum: 1, pageSize: 20 })
const groupQuery = reactive({ pageNum: 1, pageSize: 20, countryKeyword: '' })
const detailOpen = ref(false)
const detailLoading = ref(false)
const detail = ref(null)
const detailTitle = ref('')
const phStatus = '状态'
const phProduct = '产品代码'
const phCountry = '国家代码/名称'
const emptyText = '该版本暂无价格变更记录'
const emptyProducts = '该时间点暂无产品变更'
const emptyGroup = '该产品暂无明细变更'
@@ -159,6 +179,8 @@ const colAction = '操作'
const colOperator = '操作人'
const colProduct = '产品代码'
const colCountry = '国家'
const colZone = '分区'
const colWeight = '公斤段'
const colCount = '变更条数'
const colChangeSummary = '变更摘要'
const colStatus = '状态'
@@ -173,6 +195,8 @@ const lbStatus = '状态'
const lbTime = '写入时间'
const lbProduct = '产品代码'
const lbCountry = '国家'
const lbWeight = '公斤段'
const lbZone = '分区'
const statusOptions = [
{ label: '成功', value: 'SUCCESS' },
@@ -224,10 +248,12 @@ const timeTabs = computed(() => {
const productRows = computed(() => {
const minute = activeTime.value
if (!minute) return []
const kw = (query.productKeyword || '').trim().toLowerCase()
const map = new Map()
for (const g of allGroups.value || []) {
if (toMinuteKey(g && g.updateTime) !== minute) continue
const code = g.productCode || '-'
if (kw && !String(code).toLowerCase().includes(kw)) continue
let row = map.get(code)
if (!row) {
row = {
@@ -253,7 +279,7 @@ function close() {
}
function resetQuery() {
query.status = undefined
query.productKeyword = ''
reload()
}
@@ -295,6 +321,79 @@ function countryOf(row) {
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) {
const req = parseReq(row)
return Array.isArray(req.changes) ? req.changes : []
@@ -302,13 +401,19 @@ function changesOf(row) {
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(';')
const band = weightBandOf(row)
const pricePart = !list.length
? '-'
: 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(';')
if (band && band !== '-') {
return '公斤段 ' + band + (pricePart === '-' ? '' : ':' + pricePart)
}
return pricePart
}
function statusLabel(status) {
@@ -333,7 +438,6 @@ async function reload() {
const res = await listPriceLogGroups({
pageNum: 1,
pageSize: 500,
status: query.status,
psvId,
priceKind: 'SALES'
})
@@ -354,10 +458,22 @@ function openProduct(row) {
activeProduct.value = row
groupTitle.value = '变更明细 · ' + (row.productCode || '-') + (activeTime.value ? ' @ ' + activeTime.value : '')
groupQuery.pageNum = 1
groupQuery.countryKeyword = ''
groupOpen.value = true
reloadGroup()
}
function searchGroup() {
groupQuery.pageNum = 1
reloadGroup()
}
function resetGroupQuery() {
groupQuery.countryKeyword = ''
groupQuery.pageNum = 1
reloadGroup()
}
async function reloadGroup() {
const psvId = props.version && props.version.psvId
const p = activeProduct.value
@@ -368,14 +484,15 @@ async function reloadGroup() {
}
groupLoading.value = true
try {
const kw = (groupQuery.countryKeyword || '').trim()
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
updateTimePrefix: activeTime.value || p.updateTimePrefix || undefined,
countryKeyword: kw || undefined
})
groupRows.value = res.rows || []
groupTotal.value = Number(res.total) || 0
@@ -12,10 +12,10 @@
</template>
<el-tabs v-model="statusTab" class="ver-tabs">
<el-tab-pane :label="tabLabel('all')" name="all" />
<el-tab-pane :label="tabLabel('0')" name="0" />
<el-tab-pane :label="tabLabel('3')" name="3" />
<el-tab-pane :label="tabLabel('pending')" name="pending" />
<el-tab-pane :label="tabLabel('1')" name="1" />
<el-tab-pane :label="tabLabel('2')" name="2" />
<el-tab-pane :label="tabLabel('del')" name="del" />
</el-tabs>
<el-table :data="filteredVersions" border empty-text="暂无版本">
@@ -32,9 +32,12 @@
</el-table-column>
<el-table-column :label="colStatus" width="100" align="center">
<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>
</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">
<template #default="{ row }">{{ detailCount(row) }}</template>
</el-table-column>
@@ -49,27 +52,27 @@
@click="emit('view-price-log', row)"
>变更记录</el-button>
<el-button
v-if="showEditButton(row)"
v-if="!isDeletedVersion(row) && showEditButton(row)"
link
type="primary"
@click="emit('edit', row)"
>编辑</el-button>
<el-button
v-if="isDraftVersion(row)"
v-if="!isDeletedVersion(row) && isDraftVersion(row)"
link
type="success"
@click="emit('activate', row)"
v-hasPermi="editPerm"
>启动</el-button>
<el-button
v-if="isPendingVersion(row)"
v-if="!isDeletedVersion(row) && isPendingVersion(row)"
link
type="primary"
@click="emit('reschedule', row)"
v-hasPermi="editPerm"
>修改生效时间</el-button>
<el-button
v-if="isDraftVersion(row) || isPendingVersion(row) || isEffectVersion(row)"
v-if="!isDeletedVersion(row) && (isDraftVersion(row) || isPendingVersion(row) || isEffectVersion(row))"
link
type="danger"
:loading="isRemoving(row)"
@@ -102,6 +105,7 @@ const emit = defineEmits(['update:modelValue', 'create-version', 'open-details',
const title = '套餐版本'
const colVer = '版本号'
const colCreate = '创建时间'
const colEff = '生效时间'
const colExp = '失效时间'
const colStatus = '状态'
@@ -109,28 +113,42 @@ const colRows = '明细行数'
const colAction = '操作'
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 map = { all: 0, '0': 0, '3': 0, '1': 0, '2': 0 }
;(props.versions || []).forEach(v => {
const map = { all: 0, pending: 0, '1': 0, '2': 0, del: 0 }
activeVersions.value.forEach(v => {
map.all++
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
})
function tabLabel(key) {
const labels = {
all: '全部',
'0': '草稿',
'3': '待生效',
pending: '待生效',
'1': '已生效',
'2': '已失效'
'2': '已失效',
del: '已删除'
}
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) {
if (!v || !v.effectiveTime) return NaN
@@ -166,10 +184,21 @@ function sortVersions(list) {
}
const filteredVersions = computed(() => {
const list = props.versions || []
const filtered = statusTab.value === 'all'
? list
: list.filter(v => String(v.status) === statusTab.value)
if (statusTab.value === 'del') {
return sortVersions(deletedVersions.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)
})
+3 -1
View File
@@ -299,6 +299,7 @@ function findPrevVersionEffectiveTime(row) {
let prev = null
for (const v of versions) {
if (!v || v.psvId == null || String(v.psvId) === String(row.psvId)) continue
if (String(v.isDel) === '1') continue
const vs = Number(v.versionSeq) || 0
if (vs >= curSeq || !v.effectiveTime) continue
if (!prev || vs > (Number(prev.versionSeq) || 0)) prev = v
@@ -312,6 +313,7 @@ function collectActivateBoundaryTimes(excludePsvId) {
const seen = new Set()
for (const v of set.value.versions || []) {
if (!v || v.psvId == null || String(v.psvId) === String(excludePsvId)) continue
if (String(v.isDel) === '1') continue
;[v.effectiveTime, v.expireTime].forEach(t => {
const n = t ? normalizeToHour(t) : ''
if (!n || seen.has(n)) return
@@ -449,7 +451,7 @@ function submitActivate() {
function onCreateVersion() {
const priceSetId = set.value.priceSetId
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) {
proxy.$modal.msgWarning('已有草稿版本,请先启动或删除后再新建')
return