Compare commits

2 Commits

Author SHA1 Message Date
wuweihua 7807534e5d 去掉监控的价格日志 2026-11-06 10:28:55 +08:00
wuweihua e415843b14 增加价格变更日志 2026-11-06 10:11:15 +08:00
14 changed files with 513 additions and 342 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>
+26 -19
View File
@@ -1,4 +1,4 @@
-- Price change log under system monitor -- Price change log: remove monitor page menu; keep query permission for package version change log
SET NAMES utf8mb4; SET NAMES utf8mb4;
SET @monitorParent := ( SET @monitorParent := (
@@ -8,29 +8,36 @@ SET @monitorParent := (
LIMIT 1 LIMIT 1
); );
INSERT INTO sys_menu ( -- Drop legacy monitor page menu
menu_id, menu_name, parent_id, order_num, path, component, query, route_name, DELETE FROM sys_role_menu WHERE menu_id = 2350;
is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark DELETE FROM sys_menu WHERE menu_id = 2350
) OR (menu_type = 'C' AND (path = 'priceLog' OR component = 'monitor/priceLog/index'));
SELECT 2350, N'价格日志', IFNULL(@monitorParent, 2), 10,
'priceLog', 'monitor/priceLog/index', '', 'PriceLog', -- Normalize existing permission row to button (F), no page component
1, 0, 'C', '0', '0', 'monitor:priceLog:list', 'log', 'admin', sysdate(), UPDATE sys_menu
N'销售价/产品价/红线价改前改后(ES)' SET parent_id = IFNULL(@monitorParent, parent_id),
FROM DUAL menu_type = 'F',
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2350 OR perms = 'monitor:priceLog:list'); path = '',
component = NULL,
visible = '0',
status = '0',
menu_name = N'价格变更日志查询',
remark = N'套餐版本变更记录等查询权限(无监控页面)'
WHERE menu_id = 2351
OR perms = 'monitor:priceLog:list';
INSERT INTO sys_menu ( INSERT INTO sys_menu (
menu_id, menu_name, parent_id, order_num, path, component, menu_id, menu_name, parent_id, order_num, path, component,
is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark
) )
SELECT 2351, N'查询', 2350, 1, '', NULL, SELECT 2351, N'价格变更日志查询', IFNULL(@monitorParent, 2), 10, '', NULL,
1, 0, 'F', '0', '0', 'monitor:priceLog:list', '#', 'admin', sysdate() 1, 0, 'F', '0', '0', 'monitor:priceLog:list', '#', 'admin', sysdate(),
N'套餐版本变更记录等查询权限(无监控页面)'
FROM DUAL FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2351); WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2351 OR perms = 'monitor:priceLog:list');
INSERT INTO sys_role_menu (role_id, menu_id) INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, m.menu_id SELECT 1, m.menu_id
FROM (SELECT 2350 AS menu_id UNION SELECT 2351) m FROM sys_menu m
WHERE NOT EXISTS ( WHERE m.perms = 'monitor:priceLog:list'
SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
);
@@ -1,265 +0,0 @@
<template>
<div class="app-container">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
row-key="docId"
>
<template #col-priceKind="{ row }">
<el-tag size="small" :type="kindTag(row)">{{ kindLabel(row) }}</el-tag>
</template>
<template #col-actionName="{ row }">
{{ actionLabel(row) }}
</template>
<template #col-operator="{ row }">
{{ operatorOf(row) }}
</template>
<template #col-productCode="{ row }">
{{ productCodeOf(row) }}
</template>
<template #col-status="{ row }">
<el-tag size="small" :type="statusTag(row.status)">{{ statusLabel(row.status) }}</el-tag>
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="View" @click="handleView(row)" v-hasPermi="['monitor:priceLog:list']">查看</el-button>
</template>
</basic-table>
<el-drawer v-model="open" size="760px" :title="drawerTitle" append-to-body destroy-on-close>
<div v-loading="loading">
<el-empty v-if="!detail" description="暂无详情" />
<div v-else>
<el-descriptions :column="2" border size="small" class="mb12">
<el-descriptions-item label="价格类型">
<el-tag size="small" :type="kindTag(detail)">{{ kindLabel(detail) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="操作">{{ actionLabel(detail) }}</el-descriptions-item>
<el-descriptions-item label="操作人">{{ operatorOf(detail) }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag size="small" :type="statusTag(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="业务ID">{{ detail.bizId || '-' }}</el-descriptions-item>
<el-descriptions-item label="写入时间">{{ detail.updateTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="产品代码">{{ productCodeOf(detail) }}</el-descriptions-item>
<el-descriptions-item label="国家">{{ countryOf(detail) }}</el-descriptions-item>
<el-descriptions-item v-if="detail.errorMessage" label="错误信息" :span="2">
<span class="err-text">{{ detail.errorMessage }}</span>
</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="无字段变更">
<el-table-column prop="fieldLabel" label="字段" min-width="120" />
<el-table-column prop="oldValue" label="改前" min-width="140" show-overflow-tooltip />
<el-table-column prop="newValue" label="改后" min-width="140" show-overflow-tooltip />
</el-table>
</div>
<div class="json-panel">
<div class="json-panel__head">
<span>请求报文</span>
<el-button size="small" icon="CopyDocument" @click="copyJson(detail.requestJson)">复制</el-button>
</div>
<pre class="code-pre">{{ formatJson(detail.requestJson) }}</pre>
</div>
</div>
</div>
</el-drawer>
</div>
</template>
<script setup name="PriceLog">
import { listPriceLog, getPriceLog } from '@/api/monitor/priceLog'
const tableRef = ref()
const open = ref(false)
const loading = ref(false)
const detail = ref(null)
const drawerTitle = ref('价格日志详情')
const kindOptions = [
{ label: '销售价', value: 'SALES' },
{ label: '产品价', value: 'PRODUCT' },
{ label: '红线价', value: 'REDLINE' }
]
const actionOptions = [
{ label: '新增', value: 'INSERT' },
{ label: '修改', value: 'UPDATE' },
{ label: '删除', value: 'DELETE' },
{ label: '导入', value: 'IMPORT' }
]
const statusOptions = [
{ label: '成功', value: 'SUCCESS' },
{ label: '失败', value: 'FAIL' }
]
const searchColumns = [
{ label: '状态', prop: 'status', component: 'select', options: statusOptions, placeholder: '请选择状态', width: '140px', props: { clearable: true } },
{ label: '业务ID', prop: 'bizId', component: 'input', placeholder: 'SALES_/PRODUCT_/REDLINE_', width: '240px' }
]
const columns = [
{ label: '价格类型', prop: 'priceKind', align: 'center', width: 100, slot: 'priceKind' },
{ label: '操作', prop: 'actionName', align: 'center', width: 90, slot: 'actionName' },
{ label: '操作人', prop: 'operator', width: 110, slot: 'operator', showOverflowTooltip: true },
{ label: '产品代码', prop: 'productCode', minWidth: 120, slot: 'productCode', showOverflowTooltip: true },
{ label: '状态', prop: 'status', align: 'center', width: 90, slot: 'status' },
{ label: '业务ID', prop: 'bizId', minWidth: 160, showOverflowTooltip: true },
{ label: '写入时间', prop: 'updateTime', align: 'center', width: 180 },
{ label: '操作', prop: 'action', align: 'center', width: 90, fixed: 'right', slot: 'action', showInToolbar: false }
]
function queryList(params) {
return listPriceLog(params)
}
function parseReq(row) {
if (!row || !row.requestJson) return {}
try {
return typeof row.requestJson === 'object' ? row.requestJson : JSON.parse(row.requestJson)
} catch (e) {
return {}
}
}
function kindOf(row) {
const req = parseReq(row)
return req.priceKind || ''
}
function kindLabel(row) {
const v = kindOf(row)
const found = kindOptions.find(i => i.value === v)
return found ? found.label : (v || '-')
}
function kindTag(row) {
const v = kindOf(row)
if (v === 'SALES') return 'success'
if (v === 'PRODUCT') return 'warning'
if (v === 'REDLINE') return 'danger'
return 'info'
}
function actionLabel(row) {
const req = parseReq(row)
const found = actionOptions.find(i => i.value === req.action)
return found ? found.label : (req.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 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'
}
function handleView(row) {
drawerTitle.value = '价格日志 #' + (row.bizId || row.docId)
open.value = true
loading.value = true
detail.value = null
getPriceLog(row.docId).then(res => {
detail.value = res.data || row
}).finally(() => {
loading.value = false
})
}
function formatJson(val) {
if (val === null || val === undefined || val === '') return '无数据'
if (typeof val === 'object') {
try { return JSON.stringify(val, null, 2) } catch (e) { return String(val) }
}
try { return JSON.stringify(JSON.parse(val), null, 2) } catch (e) { return String(val) }
}
function copyJson(val) {
const text = formatJson(val)
const done = () => ElMessage({ message: '已复制', type: 'success', duration: 1500 })
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(done)
return
}
const ta = document.createElement('textarea')
ta.value = text
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
done()
}
</script>
<style scoped>
.mb12 { margin-bottom: 12px; }
.err-text { color: #f56c6c; word-break: break-all; }
.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;
}
.json-panel {
margin-bottom: 12px;
background: #f7f9fb;
border: 1px solid #e8ecf0;
border-radius: 6px;
overflow: hidden;
}
.json-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
font-size: 13px;
font-weight: 600;
color: #606266;
background: #fafbfc;
border-bottom: 1px solid #ebeef5;
}
.json-panel .code-pre {
margin: 0;
padding: 12px 14px;
max-height: 280px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
}
</style>
@@ -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