修改价格套餐版本
This commit is contained in:
+2
-1
@@ -78,7 +78,8 @@ public class TmsPriceController extends BaseController
|
||||
|
||||
/**
|
||||
* Sync order and persist prc_order_bill / prc_income_bill_fee.
|
||||
* Same price match as quote. Same orderNo upserts.
|
||||
* Same price match as quote (bound set + active version by bizDate, else default).
|
||||
* Same orderNo upserts.
|
||||
*/
|
||||
@PostMapping("/bill")
|
||||
public AjaxResult bill(
|
||||
|
||||
@@ -53,4 +53,10 @@ public class PrcPriceSetDetail implements Serializable
|
||||
private Date updateTime;
|
||||
/** query only: product/country/zone/weight keyword */
|
||||
private String keyword;
|
||||
/** query only: product code/name like */
|
||||
private String productKeyword;
|
||||
/** query only: country code/name like */
|
||||
private String countryKeyword;
|
||||
/** query only: zone code/name like */
|
||||
private String zoneKeyword;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ public class TmsPriceQuoteResult implements Serializable
|
||||
private String setType;
|
||||
private Integer bindPriority;
|
||||
private Long customerPricesetId;
|
||||
/** Hit package version id (prc_price_set_version.psv_id) */
|
||||
private Long psvId;
|
||||
/** Hit package version no (e.g. V1) */
|
||||
private String versionNo;
|
||||
/** Hit package detail id when package version detail matched */
|
||||
private Long psdId;
|
||||
|
||||
private String productCode;
|
||||
private String productName;
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ public interface PrcCustomerPriceSetMapper
|
||||
@Param("endDate") java.util.Date endDate);
|
||||
|
||||
/**
|
||||
* 客户在业务日有效、且套餐状态为生效(E) 的绑定,按优先级升序
|
||||
* Customer binds valid on bizDate; package not stopped. Version match is done in quote service.
|
||||
*/
|
||||
public java.util.List<PrcCustomerPriceSet> selectEffectiveByCustomer(
|
||||
@Param("customerId") Long customerId,
|
||||
|
||||
+4
-2
@@ -53,9 +53,10 @@ public interface PrcPriceSetDetailMapper
|
||||
@Param("priceSetId") Long priceSetId,
|
||||
@Param("psvId") Long psvId);
|
||||
|
||||
/** FIXED: product + country + zone + productCountryId / weight */
|
||||
/** FIXED: product + country + zone + productCountryId / weight; optional psvId scopes to version */
|
||||
public PrcPriceSetDetail selectFixedMatch(
|
||||
@Param("priceSetId") Long priceSetId,
|
||||
@Param("psvId") Long psvId,
|
||||
@Param("productCode") String productCode,
|
||||
@Param("countryCode") String countryCode,
|
||||
@Param("countryName") String countryName,
|
||||
@@ -63,8 +64,9 @@ public interface PrcPriceSetDetailMapper
|
||||
@Param("productCountryId") Long productCountryId,
|
||||
@Param("weightLabel") String weightLabel);
|
||||
|
||||
/** DISCOUNT:按产品命中明细折扣(可空) */
|
||||
/** DISCOUNT by product; optional psvId scopes to version */
|
||||
public PrcPriceSetDetail selectDiscountMatch(
|
||||
@Param("priceSetId") Long priceSetId,
|
||||
@Param("psvId") Long psvId,
|
||||
@Param("productCode") String productCode);
|
||||
}
|
||||
|
||||
+9
@@ -1,5 +1,6 @@
|
||||
package com.ruoyi.price.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.price.domain.PrcPriceSetVersion;
|
||||
@@ -24,4 +25,12 @@ public interface PrcPriceSetVersionMapper
|
||||
public int deletePrcPriceSetVersionByPriceSetId(Long priceSetId);
|
||||
|
||||
public List<Long> selectPriceSetIds();
|
||||
|
||||
/**
|
||||
* Version effective at asOf: effective_time <= asOf and (expire_time null or > asOf).
|
||||
* Prefer latest effective_time.
|
||||
*/
|
||||
public PrcPriceSetVersion selectActiveByPriceSetAt(
|
||||
@Param("priceSetId") Long priceSetId,
|
||||
@Param("asOf") Date asOf);
|
||||
}
|
||||
|
||||
+7
-4
@@ -6,11 +6,14 @@ import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* TMS freight quote service.
|
||||
* TMS freight quote / bill calc service (shared by console trial and upstream bill).
|
||||
*
|
||||
* Match order: effective customer package (bind_priority ASC) FIXED override
|
||||
* then DISCOUNT, else bms_product_country default freight/registration.
|
||||
* After calc, persist fee lines by bms_fee_type; same trial params return stored result.
|
||||
* Match order:
|
||||
* 1) Customer binds (not stopped), bind_priority ASC
|
||||
* 2) Active package version at bill-time end-of-day (effective_time / expire_time)
|
||||
* 3) FIXED detail override, else DISCOUNT on that version
|
||||
* 4) Else bms_product_country default
|
||||
* Package hit uses version surcharge snapshot when present; else product library.
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
|
||||
+187
-43
@@ -824,37 +824,101 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
reportSaveProgress(set, 90, "saving customers");
|
||||
if (set.getCustomers() != null)
|
||||
{
|
||||
customerMapper.deletePrcCustomerPriceSetByPriceSetId(set.getPriceSetId());
|
||||
Set<String> seenCustomers = new HashSet<String>();
|
||||
for (PrcCustomerPriceSet c : filterCustomers(set.getCustomers()))
|
||||
upsertCustomers(set);
|
||||
}
|
||||
reportSaveProgress(set, 96, "finishing");
|
||||
}
|
||||
|
||||
/** Upsert customer binds by customerPricesetId / customerCode; delete removed ones. */
|
||||
private void upsertCustomers(PrcPriceSet set)
|
||||
{
|
||||
PrcCustomerPriceSet cquery = new PrcCustomerPriceSet();
|
||||
cquery.setPriceSetId(set.getPriceSetId());
|
||||
List<PrcCustomerPriceSet> existing = customerMapper.selectPrcCustomerPriceSetList(cquery);
|
||||
Map<Long, PrcCustomerPriceSet> byId = new HashMap<Long, PrcCustomerPriceSet>();
|
||||
Map<String, PrcCustomerPriceSet> byCode = new HashMap<String, PrcCustomerPriceSet>();
|
||||
if (existing != null)
|
||||
{
|
||||
for (PrcCustomerPriceSet e : existing)
|
||||
{
|
||||
if (StringUtils.isNotEmpty(c.getCustomerCode()) && !seenCustomers.add(c.getCustomerCode()))
|
||||
if (e == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound"));
|
||||
continue;
|
||||
}
|
||||
validateCustomer(c, set);
|
||||
c.setCustomerPricesetId(null);
|
||||
c.setPriceSetId(set.getPriceSetId());
|
||||
c.setCreateBy(set.getCreateBy());
|
||||
if (c.getBindPriority() == null)
|
||||
if (e.getCustomerPricesetId() != null)
|
||||
{
|
||||
c.setBindPriority(1);
|
||||
byId.put(e.getCustomerPricesetId(), e);
|
||||
}
|
||||
try
|
||||
if (StringUtils.isNotEmpty(e.getCustomerCode()))
|
||||
{
|
||||
customerMapper.insertPrcCustomerPriceSet(c);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (isCustomerUniqueViolation(e))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound"));
|
||||
}
|
||||
throw e;
|
||||
byCode.put(e.getCustomerCode(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
reportSaveProgress(set, 96, "finishing");
|
||||
Set<Long> keepIds = new HashSet<Long>();
|
||||
Set<String> seenCustomers = new HashSet<String>();
|
||||
for (PrcCustomerPriceSet c : filterCustomers(set.getCustomers()))
|
||||
{
|
||||
if (StringUtils.isNotEmpty(c.getCustomerCode()) && !seenCustomers.add(c.getCustomerCode()))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound"));
|
||||
}
|
||||
validateCustomer(c, set);
|
||||
c.setPriceSetId(set.getPriceSetId());
|
||||
if (c.getBindPriority() == null)
|
||||
{
|
||||
c.setBindPriority(1);
|
||||
}
|
||||
PrcCustomerPriceSet hit = null;
|
||||
if (c.getCustomerPricesetId() != null)
|
||||
{
|
||||
hit = byId.get(c.getCustomerPricesetId());
|
||||
}
|
||||
if (hit == null && StringUtils.isNotEmpty(c.getCustomerCode()))
|
||||
{
|
||||
hit = byCode.get(c.getCustomerCode());
|
||||
}
|
||||
try
|
||||
{
|
||||
if (hit != null)
|
||||
{
|
||||
c.setCustomerPricesetId(hit.getCustomerPricesetId());
|
||||
c.setUpdateBy(set.getUpdateBy());
|
||||
customerMapper.updatePrcCustomerPriceSet(c);
|
||||
keepIds.add(hit.getCustomerPricesetId());
|
||||
}
|
||||
else
|
||||
{
|
||||
c.setCustomerPricesetId(null);
|
||||
c.setCreateBy(set.getCreateBy());
|
||||
customerMapper.insertPrcCustomerPriceSet(c);
|
||||
if (c.getCustomerPricesetId() != null)
|
||||
{
|
||||
keepIds.add(c.getCustomerPricesetId());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (isCustomerUniqueViolation(e))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound"));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
List<Long> orphanIds = new ArrayList<Long>();
|
||||
for (Long id : byId.keySet())
|
||||
{
|
||||
if (!keepIds.contains(id))
|
||||
{
|
||||
orphanIds.add(id);
|
||||
}
|
||||
}
|
||||
if (!orphanIds.isEmpty())
|
||||
{
|
||||
customerMapper.deletePrcCustomerPriceSetByIds(orphanIds.toArray(new Long[0]));
|
||||
}
|
||||
}
|
||||
|
||||
private void reportSaveProgress(PrcPriceSet set, int percent, String message)
|
||||
@@ -1466,7 +1530,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
PrcPriceSetVersion version = resolveSaveVersion(set);
|
||||
boolean pullLibrary = "Y".equalsIgnoreCase(set.getPullFromLibrary());
|
||||
// null details = leave existing rows alone; empty list = clear / replace with nothing
|
||||
boolean detailsProvided = set.getDetails() != null;
|
||||
List<PrcPriceSetDetail> details = filterDetails(set.getDetails());
|
||||
if (version == null && !detailsProvided && !pullLibrary)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (version == null && details.isEmpty() && !pullLibrary)
|
||||
{
|
||||
return;
|
||||
@@ -1476,7 +1546,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
version = new PrcPriceSetVersion();
|
||||
version.setEffectiveTime(set.getEffectiveDate());
|
||||
}
|
||||
// Draft versions may omit effective_time; set on activate (启动)
|
||||
// Draft versions may omit effective_time; set on activate
|
||||
version.setPriceSetId(set.getPriceSetId());
|
||||
version.setCreateBy(set.getCreateBy());
|
||||
version.setUpdateBy(set.getUpdateBy());
|
||||
@@ -1522,11 +1592,15 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
replaceVersionDetailsFromLibrary(set, version);
|
||||
}
|
||||
else if (detailsProvided)
|
||||
{
|
||||
upsertVersionDetails(set, version, details);
|
||||
reportSaveProgress(set, 85, "saving surcharges");
|
||||
resnapshotSurchargesIfNeeded(version, details);
|
||||
}
|
||||
else
|
||||
{
|
||||
replaceVersionDetails(set, version, details);
|
||||
reportSaveProgress(set, 85, "saving surcharges");
|
||||
snapshotVersionSurcharges(version, details);
|
||||
reportSaveProgress(set, 80, "details unchanged");
|
||||
}
|
||||
refreshVersionStatus(set.getPriceSetId());
|
||||
}
|
||||
@@ -1615,33 +1689,103 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
fillVersionChildren(version);
|
||||
}
|
||||
|
||||
private void replaceVersionDetails(PrcPriceSet set, PrcPriceSetVersion version, List<PrcPriceSetDetail> details)
|
||||
/**
|
||||
* Upsert details by psdId. Keeps existing rows when id matches; inserts new; deletes orphans
|
||||
* not present in the payload. Avoids full delete+reinsert.
|
||||
*/
|
||||
private void upsertVersionDetails(PrcPriceSet set, PrcPriceSetVersion version, List<PrcPriceSetDetail> details)
|
||||
{
|
||||
versionSurchargeMapper.deleteByPsvId(version.getPsvId());
|
||||
detailMapper.deletePrcPriceSetDetailByPsvId(version.getPsvId());
|
||||
int total = details == null ? 0 : details.size();
|
||||
int index = 0;
|
||||
for (PrcPriceSetDetail d : details)
|
||||
PrcPriceSetDetail query = new PrcPriceSetDetail();
|
||||
query.setPsvId(version.getPsvId());
|
||||
List<PrcPriceSetDetail> existing = detailMapper.selectPrcPriceSetDetailList(query);
|
||||
Map<Long, PrcPriceSetDetail> byId = new HashMap<Long, PrcPriceSetDetail>();
|
||||
if (existing != null)
|
||||
{
|
||||
validateDetail(set, d);
|
||||
d.setPsdId(null);
|
||||
d.setPriceSetId(set.getPriceSetId());
|
||||
d.setPsvId(version.getPsvId());
|
||||
detailMapper.insertPrcPriceSetDetail(d);
|
||||
index++;
|
||||
if (total > 0 && (index == total || index % 50 == 0 || index == 1))
|
||||
for (PrcPriceSetDetail e : existing)
|
||||
{
|
||||
int pct = 25 + (int) Math.floor(index * 55.0 / total);
|
||||
if (pct > 80)
|
||||
if (e != null && e.getPsdId() != null)
|
||||
{
|
||||
pct = 80;
|
||||
byId.put(e.getPsdId(), e);
|
||||
}
|
||||
reportSaveProgress(set, pct, "saving details " + index + "/" + total);
|
||||
}
|
||||
}
|
||||
Set<Long> keepIds = new HashSet<Long>();
|
||||
int total = details == null ? 0 : details.size();
|
||||
int index = 0;
|
||||
if (details != null)
|
||||
{
|
||||
for (PrcPriceSetDetail d : details)
|
||||
{
|
||||
validateDetail(set, d);
|
||||
d.setPriceSetId(set.getPriceSetId());
|
||||
d.setPsvId(version.getPsvId());
|
||||
if (d.getPsdId() != null && byId.containsKey(d.getPsdId()))
|
||||
{
|
||||
detailMapper.updatePrcPriceSetDetail(d);
|
||||
keepIds.add(d.getPsdId());
|
||||
}
|
||||
else
|
||||
{
|
||||
d.setPsdId(null);
|
||||
detailMapper.insertPrcPriceSetDetail(d);
|
||||
if (d.getPsdId() != null)
|
||||
{
|
||||
keepIds.add(d.getPsdId());
|
||||
}
|
||||
}
|
||||
index++;
|
||||
if (total > 0 && (index == total || index % 50 == 0 || index == 1))
|
||||
{
|
||||
int pct = 25 + (int) Math.floor(index * 55.0 / total);
|
||||
if (pct > 80)
|
||||
{
|
||||
pct = 80;
|
||||
}
|
||||
reportSaveProgress(set, pct, "saving details " + index + "/" + total);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Long> orphanIds = new ArrayList<Long>();
|
||||
for (Long id : byId.keySet())
|
||||
{
|
||||
if (!keepIds.contains(id))
|
||||
{
|
||||
orphanIds.add(id);
|
||||
}
|
||||
}
|
||||
if (!orphanIds.isEmpty())
|
||||
{
|
||||
for (Long id : orphanIds)
|
||||
{
|
||||
versionSurchargeMapper.deleteByPsdId(id);
|
||||
}
|
||||
detailMapper.deletePrcPriceSetDetailByIds(orphanIds.toArray(new Long[0]));
|
||||
}
|
||||
version.setDetails(details);
|
||||
}
|
||||
|
||||
/** Only rewrite surcharge snapshots for rows that explicitly sent surcharges. */
|
||||
private void resnapshotSurchargesIfNeeded(PrcPriceSetVersion version, List<PrcPriceSetDetail> details)
|
||||
{
|
||||
if (details == null || details.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<PrcPriceSetDetail> withSc = new ArrayList<PrcPriceSetDetail>();
|
||||
for (PrcPriceSetDetail d : details)
|
||||
{
|
||||
if (d != null && d.getPsdId() != null && d.getSurcharges() != null)
|
||||
{
|
||||
versionSurchargeMapper.deleteByPsdId(d.getPsdId());
|
||||
withSc.add(d);
|
||||
}
|
||||
}
|
||||
if (!withSc.isEmpty())
|
||||
{
|
||||
snapshotVersionSurcharges(version, withSc);
|
||||
}
|
||||
}
|
||||
|
||||
private void snapshotVersionSurcharges(PrcPriceSetVersion version, List<PrcPriceSetDetail> details)
|
||||
{
|
||||
if (details == null || details.isEmpty())
|
||||
|
||||
+109
-15
@@ -5,6 +5,7 @@ import java.math.RoundingMode;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -25,6 +26,8 @@ import com.ruoyi.price.domain.BmsProductCountry;
|
||||
import com.ruoyi.price.domain.PrcCustomerPriceSet;
|
||||
import com.ruoyi.price.domain.PrcPriceSet;
|
||||
import com.ruoyi.price.domain.PrcPriceSetDetail;
|
||||
import com.ruoyi.price.domain.PrcPriceSetVersion;
|
||||
import com.ruoyi.price.domain.PrcPriceSetVersionSurcharge;
|
||||
import com.ruoyi.price.domain.PrcQuote;
|
||||
import com.ruoyi.price.domain.PrcQuoteFee;
|
||||
import com.ruoyi.price.domain.PrcSurchargeRel;
|
||||
@@ -38,6 +41,8 @@ import com.ruoyi.price.mapper.BmsProductMapper;
|
||||
import com.ruoyi.price.mapper.PrcCustomerPriceSetMapper;
|
||||
import com.ruoyi.price.mapper.PrcPriceSetDetailMapper;
|
||||
import com.ruoyi.price.mapper.PrcPriceSetMapper;
|
||||
import com.ruoyi.price.mapper.PrcPriceSetVersionMapper;
|
||||
import com.ruoyi.price.mapper.PrcPriceSetVersionSurchargeMapper;
|
||||
import com.ruoyi.price.mapper.PrcQuoteFeeMapper;
|
||||
import com.ruoyi.price.mapper.PrcQuoteMapper;
|
||||
import com.ruoyi.price.mapper.PrcSurchargeRelMapper;
|
||||
@@ -47,8 +52,8 @@ import com.ruoyi.price.search.IBillSearchStore;
|
||||
import com.ruoyi.price.service.ITmsPriceQuoteService;
|
||||
|
||||
/**
|
||||
* TMS freight quote: customer package first, then product default.
|
||||
* After calc: persist fee lines by bms_fee_type; same trial params return stored rows.
|
||||
* Shared quote/bill calc: bound set + active version by bizDate, else product default.
|
||||
* Quote persists prc_quote; bill path calls calculate() then persists prc_order_bill.
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@@ -71,6 +76,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
@Autowired
|
||||
private PrcPriceSetMapper priceSetMapper;
|
||||
|
||||
@Autowired
|
||||
private PrcPriceSetVersionMapper versionMapper;
|
||||
|
||||
@Autowired
|
||||
private PrcPriceSetVersionSurchargeMapper versionSurchargeMapper;
|
||||
|
||||
@Autowired
|
||||
private PrcPriceSetDetailMapper detailMapper;
|
||||
|
||||
@@ -249,17 +260,24 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
|
||||
if (binds != null)
|
||||
{
|
||||
Date asOf = endOfDay(bizDate);
|
||||
for (PrcCustomerPriceSet bind : binds)
|
||||
{
|
||||
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(bind.getPriceSetId());
|
||||
if (set == null || !"E".equals(set.getStatus()))
|
||||
if (set == null || "S".equals(set.getStatus()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
PrcPriceSetVersion version = versionMapper.selectActiveByPriceSetAt(set.getPriceSetId(), asOf);
|
||||
if (version == null || version.getPsvId() == null)
|
||||
{
|
||||
// Bound package but no version effective at bill time -> try next bind / default
|
||||
continue;
|
||||
}
|
||||
String setType = StringUtils.isEmpty(set.getSetType()) ? TYPE_FIXED : set.getSetType();
|
||||
if (TYPE_FIXED.equals(setType))
|
||||
{
|
||||
TmsPriceQuoteResult hit = tryFixed(bind, set, base, productCode, productName, weight);
|
||||
TmsPriceQuoteResult hit = tryFixed(bind, set, version, base, productCode, productName, weight);
|
||||
if (hit != null)
|
||||
{
|
||||
stampRequestMeta(request, hit);
|
||||
@@ -268,7 +286,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
else if (TYPE_DISCOUNT.equals(setType))
|
||||
{
|
||||
TmsPriceQuoteResult hit = tryDiscount(bind, set, base, productCode, productName, weight);
|
||||
TmsPriceQuoteResult hit = tryDiscount(bind, set, version, base, productCode, productName, weight);
|
||||
if (hit != null)
|
||||
{
|
||||
stampRequestMeta(request, hit);
|
||||
@@ -334,23 +352,25 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return n;
|
||||
}
|
||||
|
||||
private TmsPriceQuoteResult tryFixed(PrcCustomerPriceSet bind, PrcPriceSet set,
|
||||
private TmsPriceQuoteResult tryFixed(PrcCustomerPriceSet bind, PrcPriceSet set, PrcPriceSetVersion version,
|
||||
BmsProductCountry base, String productCode, String productName, BigDecimal weight)
|
||||
{
|
||||
int detailCount = detailMapper.countByPriceSetId(set.getPriceSetId());
|
||||
Long psvId = version.getPsvId();
|
||||
int detailCount = detailMapper.countByPsvId(psvId);
|
||||
if (detailCount == 0)
|
||||
{
|
||||
TmsPriceQuoteResult r = buildFromRates(SRC_DEFAULT, base.getFreightPrice(), null, null,
|
||||
base.getRegistrationFee(), base, productCode, productName, weight, null);
|
||||
fillPackage(r, bind, set);
|
||||
// 套餐固定价格没有明细,使用产品默认价格
|
||||
stampVersion(r, version, null);
|
||||
// Package fixed has no version details; use product default rates
|
||||
r.setMatchMessage(MessageUtils.message("price.quote.fixedPackageNoDetails"));
|
||||
r.setCustomerId(bind.getCustomerId());
|
||||
return r;
|
||||
}
|
||||
|
||||
PrcPriceSetDetail detail = detailMapper.selectFixedMatch(
|
||||
set.getPriceSetId(), productCode,
|
||||
set.getPriceSetId(), psvId, productCode,
|
||||
base.getCountryCode(), base.getCountryName(), base.getZoneCode(),
|
||||
base.getDetailId(),
|
||||
formatWeightLabel(base.getWeightMin(), base.getWeightMax()));
|
||||
@@ -365,6 +385,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
TmsPriceQuoteResult r = buildFromRates(SRC_FIXED, detail.getFvValue(), null, null,
|
||||
detail.getFvRegistrationFee(), base, productCode, productName, weight, detail.getRoundMode());
|
||||
fillPackage(r, bind, set);
|
||||
stampVersion(r, version, detail.getPsdId());
|
||||
if (StringUtils.isNotEmpty(detail.getProductName()))
|
||||
{
|
||||
r.setProductName(detail.getProductName());
|
||||
@@ -377,25 +398,28 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
r.setProductCountryId(detail.getProductCountryId());
|
||||
}
|
||||
// 命中客户套餐固定价格明细
|
||||
r.setMatchMessage(MessageUtils.message("price.quote.hitCustomerFixedPackageDetail"));
|
||||
r.setMatchMessage(MessageUtils.message("price.quote.hitCustomerFixedPackageDetail")
|
||||
+ " [" + (version.getVersionNo() == null ? ("psv=" + psvId) : version.getVersionNo()) + "]");
|
||||
r.setCustomerId(bind.getCustomerId());
|
||||
return r;
|
||||
}
|
||||
|
||||
private TmsPriceQuoteResult tryDiscount(PrcCustomerPriceSet bind, PrcPriceSet set,
|
||||
private TmsPriceQuoteResult tryDiscount(PrcCustomerPriceSet bind, PrcPriceSet set, PrcPriceSetVersion version,
|
||||
BmsProductCountry base, String productCode, String productName, BigDecimal weight)
|
||||
{
|
||||
int detailCount = detailMapper.countByPriceSetId(set.getPriceSetId());
|
||||
Long psvId = version.getPsvId();
|
||||
int detailCount = detailMapper.countByPsvId(psvId);
|
||||
BigDecimal discount = set.getDefaultDiscount();
|
||||
String roundMode = null;
|
||||
Long psdId = null;
|
||||
if (detailCount > 0)
|
||||
{
|
||||
PrcPriceSetDetail detail = detailMapper.selectDiscountMatch(set.getPriceSetId(), productCode);
|
||||
PrcPriceSetDetail detail = detailMapper.selectDiscountMatch(set.getPriceSetId(), psvId, productCode);
|
||||
if (detail == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
psdId = detail.getPsdId();
|
||||
if (detail.getDiscountValue() != null)
|
||||
{
|
||||
discount = detail.getDiscountValue();
|
||||
@@ -419,11 +443,25 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
TmsPriceQuoteResult r = buildFromRates(SRC_DISCOUNT, rate, baseRate, discount,
|
||||
base.getRegistrationFee(), base, productCode, productName, weight, roundMode);
|
||||
fillPackage(r, bind, set);
|
||||
r.setMatchMessage("hit customer DISCOUNT package; freight = default * discount%");
|
||||
stampVersion(r, version, psdId);
|
||||
r.setMatchMessage("hit customer DISCOUNT package version "
|
||||
+ (version.getVersionNo() == null ? String.valueOf(psvId) : version.getVersionNo())
|
||||
+ "; freight = default * discount%");
|
||||
r.setCustomerId(bind.getCustomerId());
|
||||
return r;
|
||||
}
|
||||
|
||||
private void stampVersion(TmsPriceQuoteResult result, PrcPriceSetVersion version, Long psdId)
|
||||
{
|
||||
if (result == null || version == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
result.setPsvId(version.getPsvId());
|
||||
result.setVersionNo(version.getVersionNo());
|
||||
result.setPsdId(psdId);
|
||||
}
|
||||
|
||||
private TmsPriceQuoteResult buildDefault(Long customerId, BmsProductCountry base,
|
||||
String productCode, String productName, BigDecimal weight, String message)
|
||||
{
|
||||
@@ -538,6 +576,23 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
return matched;
|
||||
}
|
||||
if (result.getPsdId() != null)
|
||||
{
|
||||
List<PrcPriceSetVersionSurcharge> snaps =
|
||||
versionSurchargeMapper.selectByPsdIds(Collections.singletonList(result.getPsdId()));
|
||||
if (snaps != null && !snaps.isEmpty())
|
||||
{
|
||||
for (PrcPriceSetVersionSurcharge snap : snaps)
|
||||
{
|
||||
PrcSurchargeRel rel = toRelFromVersionSnap(snap);
|
||||
if (surchargeApplies(rel, result))
|
||||
{
|
||||
matched.add(rel);
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
PrcSurchargeRel query = new PrcSurchargeRel();
|
||||
query.setProductCode(result.getProductCode());
|
||||
List<PrcSurchargeRel> rels = surchargeRelMapper.selectPrcSurchargeRelList(query);
|
||||
@@ -555,6 +610,27 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return matched;
|
||||
}
|
||||
|
||||
private PrcSurchargeRel toRelFromVersionSnap(PrcPriceSetVersionSurcharge snap)
|
||||
{
|
||||
PrcSurchargeRel rel = new PrcSurchargeRel();
|
||||
if (snap == null)
|
||||
{
|
||||
return rel;
|
||||
}
|
||||
rel.setScId(snap.getScId());
|
||||
rel.setProductCode(snap.getProductCode());
|
||||
rel.setScName(snap.getScName());
|
||||
rel.setFeeType(snap.getFeeType());
|
||||
rel.setChargeType(snap.getChargeType());
|
||||
rel.setChargeValue(snap.getChargeValue());
|
||||
rel.setCurrency(snap.getCurrency());
|
||||
rel.setRoundMode(snap.getRoundMode());
|
||||
rel.setCountryCode(snap.getCountryCode());
|
||||
rel.setScNote(snap.getScNote());
|
||||
rel.setSsCode("on");
|
||||
return rel;
|
||||
}
|
||||
|
||||
private boolean surchargeApplies(PrcSurchargeRel rel, TmsPriceQuoteResult result)
|
||||
{
|
||||
if (rel == null)
|
||||
@@ -961,6 +1037,24 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
}
|
||||
|
||||
/** Use end of bill day so same-day version activate still matches that day's quote. */
|
||||
private static Date endOfDay(Date day)
|
||||
{
|
||||
if (day == null)
|
||||
{
|
||||
return new Date();
|
||||
}
|
||||
try
|
||||
{
|
||||
String d = new SimpleDateFormat("yyyy-MM-dd").format(day);
|
||||
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(d + " 23:59:59");
|
||||
}
|
||||
catch (ParseException e)
|
||||
{
|
||||
return day;
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveProductName(String productCode, Date bizDate)
|
||||
{
|
||||
BmsProduct p = productMapper.selectEffectiveBmsProductByCode(productCode, bizDate);
|
||||
|
||||
@@ -97,8 +97,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
from prc_customer_price_set c
|
||||
inner join prc_price_set s on s.price_set_id = c.price_set_id
|
||||
where c.customer_id = #{customerId}
|
||||
and s.status = 'E'
|
||||
and c.pr_begindate <= #{bizDate}
|
||||
and ifnull(s.status, 'D') <> 'S'
|
||||
and (c.pr_begindate is null or c.pr_begindate <= #{bizDate})
|
||||
and ifnull(c.pr_enddate, '9999-12-31') >= #{bizDate}
|
||||
order by c.bind_priority asc, c.pr_begindate desc, c.customer_priceset_id asc
|
||||
</select>
|
||||
@@ -110,8 +110,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
from prc_customer_price_set c
|
||||
inner join prc_price_set s on s.price_set_id = c.price_set_id
|
||||
where c.customer_code = #{customerCode}
|
||||
and s.status = 'E'
|
||||
and c.pr_begindate <= #{bizDate}
|
||||
and ifnull(s.status, 'D') <> 'S'
|
||||
and (c.pr_begindate is null or c.pr_begindate <= #{bizDate})
|
||||
and ifnull(c.pr_enddate, '9999-12-31') >= #{bizDate}
|
||||
order by c.bind_priority asc, c.pr_begindate desc, c.customer_priceset_id asc
|
||||
</select>
|
||||
|
||||
@@ -63,6 +63,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="zoneCode != null and zoneCode != ''">
|
||||
AND zone_code = #{zoneCode}
|
||||
</if>
|
||||
<if test="productKeyword != null and productKeyword != ''">
|
||||
AND (
|
||||
product_code like concat('%', #{productKeyword}, '%')
|
||||
or product_name like concat('%', #{productKeyword}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="countryKeyword != null and countryKeyword != ''">
|
||||
AND (
|
||||
country_code like concat('%', #{countryKeyword}, '%')
|
||||
or country_name like concat('%', #{countryKeyword}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="zoneKeyword != null and zoneKeyword != ''">
|
||||
AND (
|
||||
zone_code like concat('%', #{zoneKeyword}, '%')
|
||||
or zone_name like concat('%', #{zoneKeyword}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
AND (
|
||||
product_code like concat('%', #{keyword}, '%')
|
||||
@@ -269,6 +287,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<select id="selectFixedMatch" resultMap="PrcPriceSetDetailResult">
|
||||
<include refid="selectPrcPriceSetDetailVo"/>
|
||||
where price_set_id = #{priceSetId}
|
||||
<if test="psvId != null">
|
||||
and psv_id = #{psvId}
|
||||
</if>
|
||||
and product_code = #{productCode}
|
||||
and (
|
||||
<if test="productCountryId != null">
|
||||
@@ -306,6 +327,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<select id="selectDiscountMatch" resultMap="PrcPriceSetDetailResult">
|
||||
<include refid="selectPrcPriceSetDetailVo"/>
|
||||
where price_set_id = #{priceSetId}
|
||||
<if test="psvId != null">
|
||||
and psv_id = #{psvId}
|
||||
</if>
|
||||
and product_code = #{productCode}
|
||||
order by psd_id asc
|
||||
limit 1
|
||||
|
||||
@@ -100,4 +100,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
select distinct price_set_id from prc_price_set_version
|
||||
</select>
|
||||
|
||||
<select id="selectActiveByPriceSetAt" resultMap="PrcPriceSetVersionResult">
|
||||
<include refid="selectPrcPriceSetVersionVo"/>
|
||||
where price_set_id = #{priceSetId}
|
||||
and effective_time is not null
|
||||
and effective_time <= #{asOf}
|
||||
and (expire_time is null or expire_time > #{asOf})
|
||||
order by effective_time desc, version_seq desc, psv_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
<el-tag size="small" :type="sourceTagType(detail.matchSource)">{{ sourceLabel(detail.matchSource) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="套餐">{{ priceSetLabel(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="套餐版本">{{ detail.versionNo || (detail.psvId ? ('psv=' + detail.psvId) : '-') }}</el-descriptions-item>
|
||||
<el-descriptions-item label="币种">
|
||||
<dict-tag :options="bms_currency" :value="displayCurrency(detail.currency)" />
|
||||
</el-descriptions-item>
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
<el-descriptions-item label="套餐编号">{{ result.priceSetCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="套餐名称">{{ result.priceSetName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="套餐类型">{{ result.setType || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="套餐版本">{{ result.versionNo || (result.psvId ? ('psv=' + result.psvId) : '-') }}</el-descriptions-item>
|
||||
<el-descriptions-item label="币种">
|
||||
<dict-tag :options="bms_currency" :value="displayCurrency(result.currency)" />
|
||||
</el-descriptions-item>
|
||||
|
||||
@@ -107,7 +107,7 @@ const editInputRefs = {}
|
||||
const title = computed(() => {
|
||||
const code = props.productCode || ''
|
||||
const name = props.productName || ''
|
||||
return code + ' �� ' + name + ' �� 国家服务价格'
|
||||
return code + ' · ' + name + ' · 国家服务价格'
|
||||
})
|
||||
const hint = '黄色行=已改价;点编辑图标后修改,失焦即刻写入数据库。单位:运费 RMB/KG,挂号费 RMB/票。'
|
||||
const phSearch = '搜索国家/分区'
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="price-edit-panel" v-loading="loading">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">
|
||||
<el-button link type="primary" @click="close">
|
||||
<el-icon><arrow-left /></el-icon>
|
||||
返回详情
|
||||
</el-button>
|
||||
<i class="panel-bar" />
|
||||
<span>{{ title }}</span>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<el-button @click="close">关闭</el-button>
|
||||
<el-button type="primary" :disabled="!priceSetId" @click="close">完成</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<step-details
|
||||
v-if="loaded"
|
||||
:details="details"
|
||||
:price-set-id="priceSetId"
|
||||
:set-type="setType"
|
||||
:effective-time="version.effectiveTime"
|
||||
:version-no="version.versionNo"
|
||||
:ref-data="refData"
|
||||
header-title="编辑产品价格"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="PriceEditPanel">
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import StepDetails from './StepDetails.vue'
|
||||
import { usePriceSetRefData, useDetailCascade } from '../../composables/usePriceSetRefData'
|
||||
import { getPriceSet } from '@/api/price/set'
|
||||
|
||||
const props = defineProps({
|
||||
priceSetId: { type: [Number, String], default: undefined },
|
||||
psvId: { type: [Number, String], default: undefined }
|
||||
})
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const refData = usePriceSetRefData()
|
||||
const cascade = useDetailCascade(refData)
|
||||
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
const setType = ref('FIXED')
|
||||
const version = reactive({ psvId: undefined, versionNo: 'V1', effectiveTime: undefined })
|
||||
const details = ref([])
|
||||
|
||||
const title = computed(() => '编辑产品价格 · ' + (version.versionNo || ''))
|
||||
|
||||
function load() {
|
||||
const id = props.priceSetId
|
||||
if (!id) {
|
||||
proxy.$modal.msgError('缺少套餐ID')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
loaded.value = false
|
||||
getPriceSet(id).then(res => {
|
||||
const data = res.data || {}
|
||||
setType.value = data.setType || 'FIXED'
|
||||
|
||||
const versions = data.versions || []
|
||||
const qPsv = props.psvId
|
||||
const byQuery = qPsv != null ? versions.find(v => String(v.psvId) === String(qPsv)) : null
|
||||
const current = byQuery || data.version || versions[0] || {}
|
||||
version.psvId = current.psvId
|
||||
version.versionNo = current.versionNo || 'V1'
|
||||
version.effectiveTime = current.effectiveTime
|
||||
|
||||
const src = (current.details && current.details.length) ? current.details : (data.details || [])
|
||||
details.value = src.map(d => cascade.hydrateRow(d, setType.value))
|
||||
loaded.value = true
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refData.loadProducts().finally(load)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.price-edit-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.panel-bar {
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
background: #1890ff;
|
||||
flex: none;
|
||||
}
|
||||
.panel-title span {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.panel-actions {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.panel-body {
|
||||
padding: 16px;
|
||||
}</style>
|
||||
@@ -105,7 +105,8 @@ const props = defineProps({
|
||||
setType: { type: String, default: 'FIXED' },
|
||||
effectiveTime: { type: String, default: undefined },
|
||||
versionNo: { type: String, default: '' },
|
||||
refData: { type: Object, required: true }
|
||||
refData: { type: Object, required: true },
|
||||
headerTitle: { type: String, default: '第 2 步 · 编辑产品价格' }
|
||||
})
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
@@ -113,7 +114,6 @@ const { bms_tax_mode } = useDict('bms_tax_mode')
|
||||
const { hydrateRow } = useDetailCascade(props.refData)
|
||||
|
||||
const loadingText = '正在全量拉取产品价格…'
|
||||
const headerTitle = '第 2 步 · 编辑产品价格'
|
||||
const phKeyword = '产品代码/名称'
|
||||
const phTax = '全部税模式'
|
||||
const colCode = '产品代码'
|
||||
|
||||
@@ -9,10 +9,26 @@
|
||||
<div class="drawer-body" v-loading="loading">
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="query.keyword"
|
||||
v-model="query.productKeyword"
|
||||
clearable
|
||||
:placeholder="phSearch"
|
||||
style="width: 280px"
|
||||
:placeholder="phProduct"
|
||||
style="width: 160px"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<el-input
|
||||
v-if="isFixed"
|
||||
v-model="query.countryKeyword"
|
||||
clearable
|
||||
:placeholder="phCountry"
|
||||
style="width: 140px"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<el-input
|
||||
v-if="isFixed"
|
||||
v-model="query.zoneKeyword"
|
||||
clearable
|
||||
:placeholder="phZone"
|
||||
style="width: 140px"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
@@ -111,10 +127,14 @@ const total = ref(0)
|
||||
const query = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
keyword: ''
|
||||
productKeyword: '',
|
||||
countryKeyword: '',
|
||||
zoneKeyword: ''
|
||||
})
|
||||
|
||||
const phSearch = '产品/国家/分区/重量段'
|
||||
const phProduct = '产品代码/名称'
|
||||
const phCountry = '国家'
|
||||
const phZone = '分区'
|
||||
const colProduct = '产品'
|
||||
const colCountry = '国家'
|
||||
const colZone = '分区'
|
||||
@@ -147,7 +167,9 @@ function search() {
|
||||
}
|
||||
|
||||
function reset() {
|
||||
query.keyword = ''
|
||||
query.productKeyword = ''
|
||||
query.countryKeyword = ''
|
||||
query.zoneKeyword = ''
|
||||
query.pageNum = 1
|
||||
load()
|
||||
}
|
||||
@@ -162,7 +184,9 @@ function load() {
|
||||
loading.value = true
|
||||
listSetDetail({
|
||||
psvId,
|
||||
keyword: query.keyword || undefined,
|
||||
productKeyword: query.productKeyword || undefined,
|
||||
countryKeyword: isFixed.value ? (query.countryKeyword || undefined) : undefined,
|
||||
zoneKeyword: isFixed.value ? (query.zoneKeyword || undefined) : undefined,
|
||||
pageNum: query.pageNum,
|
||||
pageSize: query.pageSize
|
||||
}).then(res => {
|
||||
@@ -178,7 +202,9 @@ watch(
|
||||
([open]) => {
|
||||
if (open) {
|
||||
query.pageNum = 1
|
||||
query.keyword = ''
|
||||
query.productKeyword = ''
|
||||
query.countryKeyword = ''
|
||||
query.zoneKeyword = ''
|
||||
load()
|
||||
}
|
||||
}
|
||||
@@ -197,6 +223,7 @@ watch(
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.pager {
|
||||
margin-top: 12px;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<el-button v-if="active > 0 && !directStep2" @click="active--">上一步</el-button>
|
||||
<el-button v-if="active === 0" type="primary" :loading="creating" @click="goStep2">创建套餐价格</el-button>
|
||||
<el-button v-if="active === 0" type="info" :loading="saving" @click="saveDraft">保存草稿</el-button>
|
||||
<el-button v-if="active === 1" type="primary" :loading="saving" @click="saveDraft">保存</el-button>
|
||||
<el-button v-if="active === 1" type="primary" @click="finishEdit">完成</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
@@ -97,6 +97,7 @@ function mapSaveProgressMessage(message) {
|
||||
if (m === 'saving version') return '保存版本'
|
||||
if (m.startsWith('saving details ')) return '保存明细 ' + m.slice('saving details '.length)
|
||||
if (m === 'saving surcharges') return '保存附加费'
|
||||
if (m === 'details unchanged') return '明细未变更'
|
||||
if (m === 'saving customers') return '保存客户'
|
||||
if (m === 'finishing') return '收尾中'
|
||||
if (m === 'done') return '完成'
|
||||
@@ -169,6 +170,25 @@ async function goStep2() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Step2: prices already live-saved via PUT /detail; go to detail page. */
|
||||
function finishEdit() {
|
||||
if (!form.priceSetId) {
|
||||
goBack()
|
||||
return
|
||||
}
|
||||
proxy.$modal.msgSuccess('修改已实时落库')
|
||||
const obj = {
|
||||
path: '/customer/setDetail',
|
||||
query: { priceSetId: form.priceSetId }
|
||||
}
|
||||
if (proxy.$tab) {
|
||||
proxy.$tab.closeOpenPage(obj)
|
||||
} else {
|
||||
proxy.$router.push(obj)
|
||||
}
|
||||
}
|
||||
|
||||
/** Step0 only: persist basic draft (no detail wipe). */
|
||||
async function saveDraft() {
|
||||
const validBasic = await basicRef.value.validate()
|
||||
if (!validBasic) {
|
||||
@@ -190,7 +210,11 @@ async function saveDraft() {
|
||||
|
||||
function buildPayload() {
|
||||
const { basic, version } = form
|
||||
return {
|
||||
const detailRows = form.details.filter(d => d && d.productCode)
|
||||
// Price edits already persist via PUT /detail; product remove uses DELETE.
|
||||
// Omit details when all rows already have psdId so save only updates header/version.
|
||||
const hasUnsavedDetail = detailRows.some(d => d.psdId == null)
|
||||
const payload = {
|
||||
priceSetId: form.priceSetId,
|
||||
priceSetName: basic.priceSetName,
|
||||
setType: basic.setType,
|
||||
@@ -199,11 +223,17 @@ function buildPayload() {
|
||||
version: {
|
||||
psvId: copyMode.value ? undefined : version.psvId,
|
||||
versionNo: version.versionNo
|
||||
// no effectiveTime: set on activate
|
||||
},
|
||||
details: form.details.filter(d => d.productCode).map(toDetailPayload),
|
||||
customers: form.customers.filter(c => c.customerCode).map(toCustomerPayload)
|
||||
}
|
||||
}
|
||||
if (hasUnsavedDetail) {
|
||||
payload.details = detailRows.map(toDetailPayload)
|
||||
}
|
||||
// Customer bind is managed on detail page APIs; only send when present to avoid wipe.
|
||||
const customers = form.customers.filter(c => c && c.customerCode).map(toCustomerPayload)
|
||||
if (customers.length) {
|
||||
payload.customers = customers
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
<template>
|
||||
<div class="app-container" v-loading="loading">
|
||||
<set-detail-header
|
||||
:set="set"
|
||||
@back="goList"
|
||||
@disable="onDisable"
|
||||
@copy="onCopy"
|
||||
<!-- 编辑模式:整屏切换,等同独立页面 -->
|
||||
<price-edit-panel
|
||||
v-if="editVisible"
|
||||
:price-set-id="set.priceSetId"
|
||||
:psv-id="editPsvId"
|
||||
@close="closeEdit"
|
||||
/>
|
||||
|
||||
<set-basic-info-card :set="set" />
|
||||
<template v-else>
|
||||
<set-detail-header
|
||||
:set="set"
|
||||
@back="goList"
|
||||
@disable="onDisable"
|
||||
@copy="onCopy"
|
||||
/>
|
||||
|
||||
<set-customer-table :set="set" />
|
||||
<set-basic-info-card :set="set" />
|
||||
|
||||
<set-version-table
|
||||
v-model="currentPsvId"
|
||||
:versions="set.versions || []"
|
||||
@copy-version="onCopyVersion"
|
||||
@open-details="onOpenVersionDetails"
|
||||
@activate="onActivateVersion"
|
||||
@edit="onEditVersion"
|
||||
@remove="onRemoveVersion"
|
||||
/>
|
||||
<set-customer-table :set="set" />
|
||||
|
||||
<set-version-detail-drawer
|
||||
v-model="detailDrawerVisible"
|
||||
:version="detailVersion"
|
||||
:set-type="set.setType"
|
||||
/>
|
||||
<set-version-table
|
||||
v-model="currentPsvId"
|
||||
:versions="set.versions || []"
|
||||
@copy-version="onCopyVersion"
|
||||
@open-details="onOpenVersionDetails"
|
||||
@activate="onActivateVersion"
|
||||
@edit="onEditVersion"
|
||||
@remove="onRemoveVersion"
|
||||
/>
|
||||
|
||||
<set-version-detail-drawer
|
||||
v-model="detailDrawerVisible"
|
||||
:version="detailVersion"
|
||||
:set-type="set.setType"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -35,6 +45,7 @@ import SetBasicInfoCard from './components/detail/SetBasicInfoCard.vue'
|
||||
import SetCustomerTable from './components/detail/SetCustomerTable.vue'
|
||||
import SetVersionTable from './components/detail/SetVersionTable.vue'
|
||||
import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.vue'
|
||||
import PriceEditPanel from './components/create/PriceEditPanel.vue'
|
||||
import { usePriceSetRefData } from './composables/usePriceSetRefData'
|
||||
import {
|
||||
getPriceSet, disablePriceSet, copyPriceSet, activatePriceSetVersion, copyPriceSetVersion, deletePriceSetVersion
|
||||
@@ -49,6 +60,8 @@ const set = ref({})
|
||||
const currentPsvId = ref()
|
||||
const detailDrawerVisible = ref(false)
|
||||
const detailVersion = ref(null)
|
||||
const editVisible = ref(false)
|
||||
const editPsvId = ref(undefined)
|
||||
|
||||
function onOpenVersionDetails(row) {
|
||||
detailVersion.value = row || null
|
||||
@@ -102,10 +115,8 @@ function onCopyVersion(row) {
|
||||
const newPsvId = res.data
|
||||
load()
|
||||
if (newPsvId != null) {
|
||||
proxy.$router.push({
|
||||
path: '/customer/setCreate',
|
||||
query: { priceSetId: set.value.priceSetId, psvId: newPsvId, step: '2' }
|
||||
})
|
||||
editPsvId.value = newPsvId
|
||||
editVisible.value = true
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
@@ -119,9 +130,14 @@ function onRemoveVersion(row) {
|
||||
}).catch(() => {})
|
||||
}
|
||||
function onEditVersion(row) {
|
||||
const q = { priceSetId: set.value.priceSetId, step: '2' }
|
||||
if (row && row.psvId != null) q.psvId = row.psvId
|
||||
proxy.$router.push({ path: '/customer/setCreate', query: q })
|
||||
editPsvId.value = row && row.psvId != null ? row.psvId : undefined
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
/** 关闭内嵌编辑面板,刷新版本列表(明细数、状态可能变化) */
|
||||
function closeEdit() {
|
||||
editVisible.value = false
|
||||
load()
|
||||
}
|
||||
function goList() {
|
||||
const obj = { path: '/customer/set' }
|
||||
|
||||
@@ -33,6 +33,16 @@
|
||||
<el-tag :type="statusTag(row)" size="small">{{ statusLabel(row) }}</el-tag>
|
||||
</template>
|
||||
|
||||
<template #col-activeDetail="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:loading="activeLoadingId === row.priceSetId"
|
||||
@click="handleActiveDetail(row)"
|
||||
v-hasPermi="['price:set:query']"
|
||||
>查看明细</el-button>
|
||||
</template>
|
||||
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" @click="handleDetail(row)" v-hasPermi="['price:set:query']">详情</el-button>
|
||||
<el-button
|
||||
@@ -44,14 +54,21 @@
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
<set-version-detail-drawer
|
||||
v-model="drawerVisible"
|
||||
:version="activeVersion"
|
||||
:set-type="drawerSetType"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="PriceSet">
|
||||
import {
|
||||
listPriceSet, disablePriceSet, copyPriceSet
|
||||
listPriceSet, disablePriceSet, copyPriceSet, getPriceSet
|
||||
} from '@/api/price/set'
|
||||
import { parseTime } from '@/utils/ruoyi'
|
||||
import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.vue'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
@@ -88,6 +105,7 @@ const columns = [
|
||||
{ label: '套餐名称', prop: 'priceSetName', minWidth: 160 },
|
||||
{ label: '类型', prop: 'setType', width: 140 },
|
||||
{ label: '说明', prop: 'priceSetNote', minWidth: 160 },
|
||||
{ label: '生效明细', prop: 'activeDetail', width: 100 },
|
||||
{ label: '操作', prop: 'action', width: 160, showInToolbar: false }
|
||||
]
|
||||
|
||||
@@ -119,6 +137,30 @@ function handleDetail(row) {
|
||||
proxy.$router.push({ path: '/customer/setDetail', query: { priceSetId: row.priceSetId } })
|
||||
}
|
||||
|
||||
/** 生效明细:拉详情找生效版本(status=1)弹抽屉 */
|
||||
const drawerVisible = ref(false)
|
||||
const activeVersion = ref(null)
|
||||
const drawerSetType = ref('FIXED')
|
||||
const activeLoadingId = ref(null)
|
||||
|
||||
function handleActiveDetail(row) {
|
||||
activeLoadingId.value = row.priceSetId
|
||||
getPriceSet(row.priceSetId).then(res => {
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const active = versions.find(v => String(v.status) === '1')
|
||||
if (!active || active.psvId == null) {
|
||||
proxy.$modal.msgWarning('该套餐暂无生效版本')
|
||||
return
|
||||
}
|
||||
drawerSetType.value = row.setType
|
||||
activeVersion.value = { psvId: active.psvId, versionNo: active.versionNo }
|
||||
drawerVisible.value = true
|
||||
}).finally(() => {
|
||||
activeLoadingId.value = null
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function refresh() {
|
||||
tableRef.value?.refresh()
|
||||
|
||||
Reference in New Issue
Block a user