修改产品套餐
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.bms.web.controller.price;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -18,6 +19,8 @@ import com.bms.common.core.controller.BaseController;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.page.TableDataInfo;
|
||||
import com.bms.common.enums.BusinessType;
|
||||
import com.bms.common.utils.DateUtils;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.price.domain.PrcOrderBill;
|
||||
import com.bms.price.domain.PrcQuote;
|
||||
import com.bms.price.domain.vo.AddIncomeBillFeeRequest;
|
||||
@@ -55,6 +58,22 @@ public class BillingController extends BaseController
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Products of the customer's bound effective package version (trial product dropdown).
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:quote')")
|
||||
@GetMapping("/quote/products")
|
||||
public AjaxResult boundProducts(@RequestParam("customerCode") String customerCode,
|
||||
@RequestParam(value = "bizDate", required = false) String bizDate)
|
||||
{
|
||||
Date asOf = null;
|
||||
if (StringUtils.isNotEmpty(bizDate))
|
||||
{
|
||||
asOf = DateUtils.parseDate(bizDate);
|
||||
}
|
||||
return success(tmsPriceQuoteService.listBoundPackageProducts(customerCode, asOf));
|
||||
}
|
||||
|
||||
/**
|
||||
* Historical quote detail with fee lines.
|
||||
*/
|
||||
|
||||
@@ -193,6 +193,15 @@ public class PriceSetController extends BaseController
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/** All detail rows for one product (no page); used by edit-price drawer. */
|
||||
@PreAuthorize("@ss.hasPermi('price:set:list')")
|
||||
@GetMapping("/detail/byProduct")
|
||||
public AjaxResult detailByProduct(@RequestParam("psvId") Long psvId,
|
||||
@RequestParam("productCode") String productCode)
|
||||
{
|
||||
return success(priceSetService.selectDetailListByProduct(psvId, productCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本明细搜索下拉选项:按版本去重后的 产品/国家/分区
|
||||
*/
|
||||
@@ -232,6 +241,22 @@ public class PriceSetController extends BaseController
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async pull product library into version details. Returns taskId; poll /save/task/{taskId}.
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:set:edit') or @ss.hasPermi('price:set:forceEdit')")
|
||||
@Log(title = "\u5957\u9910\u62c9\u53d6\u4ea7\u54c1", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/detail/pullFromLibrary")
|
||||
public AjaxResult pullFromLibrary(@RequestParam("priceSetId") Long priceSetId,
|
||||
@RequestParam(value = "psvId", required = false) Long psvId,
|
||||
@RequestParam(value = "force", required = false, defaultValue = "false") boolean force)
|
||||
{
|
||||
String taskId = priceSetService.submitPullFromLibraryAsync(priceSetId, psvId, force, getUsername());
|
||||
Map<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("taskId", taskId);
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async batch write of detail surcharge snapshots. Returns taskId; poll /save/task/{taskId}.
|
||||
*/
|
||||
|
||||
@@ -78,6 +78,8 @@ price.set.import.fixedOnly=\u4ec5\u56fa\u5b9a\u4ef7\u5957\u9910\u652f\u6301\u5bf
|
||||
price.set.import.noDetails=\u5957\u9910\u6682\u65e0\u4ef7\u683c\u660e\u7ec6\uff0c\u8bf7\u5148\u62c9\u53d6\u4ea7\u54c1\u4ef7
|
||||
price.set.import.noPriceRows=\u4ef7\u8868\u672a\u89e3\u6790\u5230\u6709\u6548\u4ef7\u683c\u884c
|
||||
price.set.import.success=\u5bfc\u5165\u5b8c\u6210\uff1a\u66f4\u65b0 {0} \u6761\uff0c\u672a\u5339\u914d {1} \u6761\uff0c\u5957\u9910\u65e0\u5bf9\u5e94\u4ea7\u54c1 {2} \u4e2a
|
||||
price.set.import.upsertSuccess=\u5bfc\u5165\u5b8c\u6210\uff1a\u65b0\u589e {0} \u6761\uff0c\u66f4\u65b0 {1} \u6761\uff0c\u8df3\u8fc7\u7a7a\u8fd0\u8d39/\u6302\u53f7\u8d39 {2} \u6761
|
||||
price.set.pull.success=\u62c9\u53d6\u5b8c\u6210\uff1a\u5df2\u5199\u5165 {0} \u6761\u5957\u9910\u660e\u7ec6
|
||||
price.set.customer.notExists=客户绑定不存在
|
||||
price.set.customer.unbindAudit=审核中的套餐不允许解绑客户
|
||||
price.set.customer.customerId.empty=客户ID不能为空
|
||||
|
||||
@@ -17,6 +17,8 @@ public class PrcPriceSetDetail implements Serializable
|
||||
private Long priceSetId;
|
||||
/** prc_price_set_version.psv_id */
|
||||
private Long psvId;
|
||||
/** prc_price_set_product.psp_id */
|
||||
private Long pspId;
|
||||
private String productCode;
|
||||
private String productName;
|
||||
private String countryCode;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 价格套餐产品主表 prc_price_set_product(与产品库快照独立)
|
||||
*/
|
||||
@Data
|
||||
public class PrcPriceSetProduct implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 套餐产品主表id */
|
||||
private Long pspId;
|
||||
/** 价格套餐id */
|
||||
private Long priceSetId;
|
||||
/** 套餐版本id */
|
||||
private Long psvId;
|
||||
/** 产品对接代码 */
|
||||
private String productCode;
|
||||
/** 产品名称 */
|
||||
private String productName;
|
||||
/** 时效等级 */
|
||||
private String serviceLevel;
|
||||
/** 走货属性 */
|
||||
private String cargoAttr;
|
||||
/** 目的区域 */
|
||||
private String destRegion;
|
||||
/** 更新说明 */
|
||||
private String updateNote;
|
||||
/** TAX_INCLUDED / TAX_EXCLUDED */
|
||||
private String taxMode;
|
||||
/** 来源产品库 product.product_id */
|
||||
private Long sourceProductId;
|
||||
/** 来源Excel sheet */
|
||||
private String sourceSheet;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -28,5 +28,6 @@ public class PrcPriceSetVersion extends BaseEntity
|
||||
private String status;
|
||||
|
||||
private List<PrcPriceSetDetail> details;
|
||||
private List<PrcPriceSetProduct> products;
|
||||
private List<PrcPriceSetVersionSurcharge> surcharges;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ public interface PrcPriceSetMapper
|
||||
{
|
||||
public PrcPriceSet selectPrcPriceSetById(Long priceSetId);
|
||||
|
||||
/** Header only (no product/customer count subqueries). */
|
||||
public PrcPriceSet selectPrcPriceSetHeaderById(Long priceSetId);
|
||||
|
||||
public List<PrcPriceSet> selectPrcPriceSetList(PrcPriceSet set);
|
||||
|
||||
public int insertPrcPriceSet(PrcPriceSet set);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.bms.price.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.price.domain.PrcPriceSetProduct;
|
||||
|
||||
/**
|
||||
* Package intermediate product mapper
|
||||
*/
|
||||
public interface PrcPriceSetProductMapper
|
||||
{
|
||||
public PrcPriceSetProduct selectPrcPriceSetProductById(Long pspId);
|
||||
|
||||
public List<PrcPriceSetProduct> selectPrcPriceSetProductList(PrcPriceSetProduct query);
|
||||
|
||||
public PrcPriceSetProduct selectByPsvIdAndCode(@Param("psvId") Long psvId, @Param("productCode") String productCode);
|
||||
|
||||
public int insertPrcPriceSetProduct(PrcPriceSetProduct row);
|
||||
|
||||
public int updatePrcPriceSetProduct(PrcPriceSetProduct row);
|
||||
|
||||
public int deleteByPsvId(@Param("psvId") Long psvId);
|
||||
|
||||
public int deleteByPsvIdAndCode(@Param("psvId") Long psvId, @Param("productCode") String productCode);
|
||||
|
||||
/** Copy active library products into this version (ignore duplicates). */
|
||||
public int insertFromLibrary(@Param("priceSetId") Long priceSetId, @Param("psvId") Long psvId);
|
||||
|
||||
/** Copy set products from one version to another. */
|
||||
public int copyFromVersion(@Param("srcPsvId") Long srcPsvId, @Param("newPsvId") Long newPsvId);
|
||||
|
||||
/** Bind detail.psp_id from set product by psv + product_code. */
|
||||
public int bindDetailPspByPsvId(@Param("psvId") Long psvId);
|
||||
}
|
||||
@@ -60,6 +60,11 @@ public interface IPrcPriceSetService
|
||||
|
||||
public List<PrcPriceSetDetail> selectDetailList(PrcPriceSetDetail detail);
|
||||
|
||||
/**
|
||||
* Load all detail rows for one product under a version (no pagination).
|
||||
*/
|
||||
public List<PrcPriceSetDetail> selectDetailListByProduct(Long psvId, String productCode);
|
||||
|
||||
/**
|
||||
* 版本明细搜索下拉选项:按版本去重后的 产品/国家/分区 列表。
|
||||
* 返回 { products: [{code,name}], countries: [{code,name}], zones: [{code,name}] }
|
||||
@@ -105,11 +110,20 @@ public interface IPrcPriceSetService
|
||||
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName) throws Exception;
|
||||
|
||||
/**
|
||||
* Sync worker: parse bytes and update FIXED detail prices matched by
|
||||
* product + country + zone(if any) + weight band.
|
||||
* Sync worker: parse bytes and upsert FIXED detail prices matched by
|
||||
* product + country + zone(if any) + weight band (insert when missing).
|
||||
*/
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName) throws Exception;
|
||||
|
||||
/** Sync worker with optional taskId for progress reporting. */
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName, String taskId) throws Exception;
|
||||
|
||||
/**
|
||||
* Submit async pull of product library into version details.
|
||||
* @return taskId for polling getSaveTask
|
||||
*/
|
||||
public String submitPullFromLibraryAsync(Long priceSetId, Long psvId, boolean force, String operName);
|
||||
|
||||
/** Sync worker: replace (or skip) version details from product / product_country. */
|
||||
public String pullFromLibrary(Long priceSetId, Long psvId, boolean force, String operName, String taskId);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.bms.price.service;
|
||||
|
||||
import com.bms.price.domain.PrcPriceSetProduct;
|
||||
import com.bms.price.domain.PrcQuote;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteResult;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -27,4 +29,11 @@ public interface ITmsPriceQuoteService
|
||||
List<PrcQuote> selectQuoteList(PrcQuote query);
|
||||
|
||||
TmsPriceQuoteResult getQuote(String quoteId);
|
||||
|
||||
/**
|
||||
* Products under the customer's bound effective package version (for trial dropdown).
|
||||
* @param customerCode required
|
||||
* @param bizDate optional; default today (same as-of window as quote)
|
||||
*/
|
||||
List<PrcPriceSetProduct> listBoundPackageProducts(String customerCode, Date bizDate);
|
||||
}
|
||||
|
||||
+361
-104
@@ -22,20 +22,24 @@ import com.bms.common.utils.DateUtils;
|
||||
import com.bms.common.utils.MessageUtils;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.common.utils.uuid.IdUtils;
|
||||
import com.bms.price.domain.BmsProduct;
|
||||
import com.bms.price.domain.BmsProductCountry;
|
||||
import com.bms.price.domain.PrcCustomerPriceSet;
|
||||
import com.bms.price.domain.PrcPriceSet;
|
||||
import com.bms.price.domain.PrcPriceSetCheckItem;
|
||||
import com.bms.price.domain.PrcPriceSetDetail;
|
||||
import com.bms.price.domain.PrcPriceSetProduct;
|
||||
import com.bms.price.domain.PrcPriceSetSaveTask;
|
||||
import com.bms.price.domain.PrcPriceSetVersion;
|
||||
import com.bms.price.domain.PrcPriceSetVersionSurcharge;
|
||||
import com.bms.price.domain.PrcSurcharge;
|
||||
import com.bms.price.domain.PrcSurchargeRel;
|
||||
import com.bms.price.mapper.BmsProductCountryMapper;
|
||||
import com.bms.price.mapper.BmsProductMapper;
|
||||
import com.bms.price.mapper.PrcCustomerPriceSetMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetDetailMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetProductMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetVersionMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetVersionSurchargeMapper;
|
||||
import com.bms.price.mapper.PrcSurchargeMapper;
|
||||
@@ -90,6 +94,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
@Autowired
|
||||
private BmsProductCountryMapper productCountryMapper;
|
||||
|
||||
@Autowired
|
||||
private PrcPriceSetProductMapper setProductMapper;
|
||||
|
||||
@Autowired
|
||||
private BmsProductMapper productMapper;
|
||||
|
||||
@Lazy
|
||||
@Autowired
|
||||
private IBmsProductService productService;
|
||||
@@ -399,9 +409,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
nv.setCreateBy(createBy);
|
||||
versionMapper.insertPrcPriceSetVersion(nv);
|
||||
|
||||
setProductMapper.copyFromVersion(sourcePsvId, nv.getPsvId());
|
||||
int detailRows = detailMapper.copyDetailsFromVersion(sourcePsvId, nv.getPsvId());
|
||||
if (detailRows > 0)
|
||||
{
|
||||
setProductMapper.bindDetailPspByPsvId(nv.getPsvId());
|
||||
versionSurchargeMapper.copySurchargesFromVersion(sourcePsvId, nv.getPsvId(), createBy);
|
||||
}
|
||||
refreshVersionStatus(set.getPriceSetId());
|
||||
@@ -428,6 +440,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
requireSet(version.getPriceSetId());
|
||||
versionSurchargeMapper.deleteByPsvId(psvId);
|
||||
detailMapper.deletePrcPriceSetDetailByPsvId(psvId);
|
||||
setProductMapper.deleteByPsvId(psvId);
|
||||
int rows = versionMapper.deletePrcPriceSetVersionByIds(new Long[] { psvId });
|
||||
refreshVersionStatus(version.getPriceSetId());
|
||||
return rows;
|
||||
@@ -587,16 +600,16 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
if (details.isEmpty())
|
||||
{
|
||||
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "PASS",
|
||||
"\u65e0\u5957\u9910\u660e\u7ec6\uff1a\u8ba1\u8d39\u53d6 product / product_country \u9ed8\u8ba4\u8fd0\u8d39\u4e0e\u6302\u53f7\u8d39"));
|
||||
items.add(new PrcPriceSetCheckItem("FIXED \u660e\u7ec6\u5b8c\u6574\u6027", "FAIL",
|
||||
"\u65e0\u5957\u9910\u660e\u7ec6\uff1a\u56fa\u5b9a\u4ef7\u987b\u5148\u62c9\u53d6\u4ea7\u54c1\u5217\u8868\u6216\u5bfc\u5165 Excel \u5efa\u4ef7"));
|
||||
}
|
||||
else if (incomplete > 0)
|
||||
{
|
||||
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "FAIL", incomplete + " \u6761\u660e\u7ec6\u7f3a\u5c11\u4ea7\u54c1/\u56fd\u5bb6/\u91cd\u91cf\u6bb5"));
|
||||
items.add(new PrcPriceSetCheckItem("FIXED \u660e\u7ec6\u5b8c\u6574\u6027", "FAIL", incomplete + " \u6761\u660e\u7ec6\u7f3a\u5c11\u4ea7\u54c1/\u56fd\u5bb6/\u91cd\u91cf\u6bb5"));
|
||||
}
|
||||
else
|
||||
{
|
||||
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "PASS", details.size() + " \u6761\u8986\u76d6\u660e\u7ec6\uff08\u672a\u8986\u76d6\u7684\u4ea7\u54c1\u56fd\u5bb6\u91cd\u91cf\u6bb5\u4ecd\u8d70\u9ed8\u8ba4\u4ef7\uff09"));
|
||||
items.add(new PrcPriceSetCheckItem("FIXED \u660e\u7ec6\u5b8c\u6574\u6027", "PASS", details.size() + " \u6761\u5957\u9910\u9500\u552e\u4ef7\u660e\u7ec6\uff08\u4e0d\u56de\u9000 product_country\uff09"));
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -673,11 +686,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
|
||||
if (TYPE_FIXED.equals(setType) && !details.isEmpty())
|
||||
{
|
||||
items.add(new PrcPriceSetCheckItem("\u8fd0\u8d39\u5355\u4ef7 vs \u9ed8\u8ba4\u7b49\u7ea7\u4ef7\u504f\u5dee", "INFO", "\u6709\u8986\u76d6\u660e\u7ec6\u65f6\u4f18\u5148\u53d6\u5957\u9910\u56fa\u5b9a\u4ef7\uff1b\u672a\u547d\u4e2d\u5219\u56de\u9000 product_country"));
|
||||
items.add(new PrcPriceSetCheckItem("\u8fd0\u8d39\u5355\u4ef7 vs \u9ed8\u8ba4\u7b49\u7ea7\u4ef7\u504f\u5dee", "INFO",
|
||||
"\u56fa\u5b9a\u4ef7\u4ec5\u4f7f\u7528\u5957\u9910\u660e\u7ec6\uff1b\u672a\u547d\u4e2d\u5219\u4e0d\u8ba1\u8d39\uff08\u4e0d\u56de\u9000 product_country\uff09"));
|
||||
}
|
||||
else if (TYPE_FIXED.equals(setType))
|
||||
{
|
||||
items.add(new PrcPriceSetCheckItem("\u8fd0\u8d39\u5355\u4ef7 vs \u9ed8\u8ba4\u7b49\u7ea7\u4ef7\u504f\u5dee", "INFO", "\u65e0\u5957\u9910\u660e\u7ec6\uff0c\u5168\u90e8\u8d70 product_country \u9ed8\u8ba4\u8fd0\u8d39/\u6302\u53f7\u8d39"));
|
||||
items.add(new PrcPriceSetCheckItem("\u8fd0\u8d39\u5355\u4ef7 vs \u9ed8\u8ba4\u7b49\u7ea7\u4ef7\u504f\u5dee", "INFO",
|
||||
"\u65e0\u5957\u9910\u660e\u7ec6\u65f6\u56fa\u5b9a\u4ef7\u4e0d\u8ba1\u8d39\uff0c\u8bf7\u5148\u62c9\u53d6\u6216\u5bfc\u5165\u5efa\u4ef7"));
|
||||
}
|
||||
else if (TYPE_DISCOUNT.equals(setType))
|
||||
{
|
||||
@@ -695,6 +710,19 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PrcPriceSetDetail> selectDetailListByProduct(Long psvId, String productCode)
|
||||
{
|
||||
if (psvId == null || StringUtils.isEmpty(productCode))
|
||||
{
|
||||
return new ArrayList<PrcPriceSetDetail>();
|
||||
}
|
||||
PrcPriceSetDetail q = new PrcPriceSetDetail();
|
||||
q.setPsvId(psvId);
|
||||
q.setProductCode(productCode.trim());
|
||||
return selectDetailList(q);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> selectDetailOptions(Long psvId)
|
||||
{
|
||||
@@ -797,6 +825,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
assertChildEditable(db);
|
||||
validateDetail(db, detail);
|
||||
detail.setPsvId(resolvePsvId(detail.getPriceSetId(), detail.getPsvId()));
|
||||
detail.setPspId(lookupSetProductId(detail.getPsvId(), detail.getProductCode()));
|
||||
int rows = detailMapper.insertPrcPriceSetDetail(detail);
|
||||
PrcPriceSetVersion ver = new PrcPriceSetVersion();
|
||||
ver.setPsvId(detail.getPsvId());
|
||||
@@ -835,6 +864,56 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String submitPullFromLibraryAsync(Long priceSetId, Long psvId, boolean force, String operName)
|
||||
{
|
||||
PrcPriceSet db = requireSet(priceSetId);
|
||||
assertChildEditable(db);
|
||||
if (psvId == null)
|
||||
{
|
||||
psvId = resolveCurrentPsvId(db);
|
||||
}
|
||||
if (psvId == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.version.notExists"));
|
||||
}
|
||||
String taskId = IdUtils.fastSimpleUUID();
|
||||
saveAsyncRunner.put(taskId, PrcPriceSetSaveTask.STATUS_PENDING, 0, "queued", priceSetId, null);
|
||||
saveAsyncRunner.runPullLibrary(taskId, priceSetId, psvId, force, operName);
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String pullFromLibrary(Long priceSetId, Long psvId, boolean force, String operName, String taskId)
|
||||
{
|
||||
PrcPriceSet set = requireSet(priceSetId);
|
||||
assertChildEditable(set);
|
||||
set.setUpdateBy(operName);
|
||||
set.setCreateBy(operName);
|
||||
set.setSaveTaskId(taskId);
|
||||
set.setPullFromLibrary("Y");
|
||||
set.setForcePullLibrary(force ? "Y" : "N");
|
||||
if (psvId == null)
|
||||
{
|
||||
psvId = resolveCurrentPsvId(set);
|
||||
}
|
||||
PrcPriceSetVersion version = null;
|
||||
if (psvId != null)
|
||||
{
|
||||
version = versionMapper.selectPrcPriceSetVersionById(psvId);
|
||||
}
|
||||
if (version == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.version.notExists"));
|
||||
}
|
||||
reportImportProgress(taskId, priceSetId, 20, "pulling library");
|
||||
replaceVersionDetailsFromLibrary(set, version);
|
||||
reportImportProgress(taskId, priceSetId, 95, "finishing");
|
||||
int count = detailMapper.countByPsvId(version.getPsvId());
|
||||
return MessageUtils.message("price.set.pull.success", Integer.valueOf(count));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName) throws Exception
|
||||
@@ -856,19 +935,24 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
psvId = resolveCurrentPsvId(db);
|
||||
}
|
||||
if (psvId == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.version.notExists"));
|
||||
}
|
||||
reportImportProgress(taskId, priceSetId, 20, "parsing excel");
|
||||
Map<String, List<BmsProductCountry>> byProduct = productService.parseQuoteExcelByProduct(bytes, filename);
|
||||
reportImportProgress(taskId, priceSetId, 50, "matching details");
|
||||
if (byProduct == null || byProduct.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
|
||||
}
|
||||
|
||||
PrcPriceSetDetail q = new PrcPriceSetDetail();
|
||||
q.setPriceSetId(priceSetId);
|
||||
if (psvId != null)
|
||||
{
|
||||
q.setPsvId(psvId);
|
||||
}
|
||||
q.setPsvId(psvId);
|
||||
List<PrcPriceSetDetail> details = detailMapper.selectPrcPriceSetDetailList(q);
|
||||
if (details == null || details.isEmpty())
|
||||
if (details == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.noDetails"));
|
||||
details = new ArrayList<PrcPriceSetDetail>();
|
||||
}
|
||||
Map<String, List<PrcPriceSetDetail>> detailIndex = new HashMap<String, List<PrcPriceSetDetail>>();
|
||||
for (PrcPriceSetDetail d : details)
|
||||
@@ -886,61 +970,139 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
list.add(d);
|
||||
}
|
||||
|
||||
int updated = 0;
|
||||
int unmatched = 0;
|
||||
int skippedProduct = 0;
|
||||
int inserted = 0;
|
||||
int skippedFee = 0;
|
||||
Set<Long> touched = new HashSet<Long>();
|
||||
int productTotal = byProduct.size();
|
||||
int productIndex = 0;
|
||||
for (Map.Entry<String, List<BmsProductCountry>> e : byProduct.entrySet())
|
||||
{
|
||||
productIndex++;
|
||||
String productCode = e.getKey();
|
||||
reportImportProgress(taskId, priceSetId,
|
||||
20 + (int) Math.floor(70.0 * productIndex / Math.max(productTotal, 1)),
|
||||
"importing " + productCode + " (" + productIndex + "/" + productTotal + ")");
|
||||
List<PrcPriceSetDetail> candidates = detailIndex.get(productCode);
|
||||
if (candidates == null || candidates.isEmpty())
|
||||
if (candidates == null)
|
||||
{
|
||||
skippedProduct++;
|
||||
continue;
|
||||
candidates = new ArrayList<PrcPriceSetDetail>();
|
||||
detailIndex.put(productCode, candidates);
|
||||
}
|
||||
String productName = resolveImportProductName(psvId, productCode, candidates);
|
||||
// Excel import is one of the only two paths that create set-product rows
|
||||
Long pspId = ensureSetProduct(priceSetId, psvId, productCode, productName);
|
||||
for (BmsProductCountry row : e.getValue())
|
||||
{
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BigDecimal freight = row.getFreightPrice();
|
||||
BigDecimal registration = row.getRegistrationFee();
|
||||
List<PrcPriceSetDetail> hits = matchImportDetails(candidates, row);
|
||||
if (hits.isEmpty())
|
||||
if (!hits.isEmpty())
|
||||
{
|
||||
unmatched++;
|
||||
continue;
|
||||
}
|
||||
for (PrcPriceSetDetail hit : hits)
|
||||
{
|
||||
if (hit.getPsdId() == null || touched.contains(hit.getPsdId()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Empty import freight/registration: do not overwrite existing detail values.
|
||||
BigDecimal freight = row.getFreightPrice();
|
||||
BigDecimal registration = row.getRegistrationFee();
|
||||
if (freight == null && registration == null)
|
||||
{
|
||||
skippedFee++;
|
||||
continue;
|
||||
}
|
||||
detailMapper.updateDetailPrices(hit.getPsdId(), freight, registration);
|
||||
if (freight != null)
|
||||
for (PrcPriceSetDetail hit : hits)
|
||||
{
|
||||
hit.setFvValue(freight);
|
||||
if (hit.getPsdId() == null || touched.contains(hit.getPsdId()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
detailMapper.updateDetailPrices(hit.getPsdId(), freight, registration);
|
||||
if (freight != null)
|
||||
{
|
||||
hit.setFvValue(freight);
|
||||
}
|
||||
if (registration != null)
|
||||
{
|
||||
hit.setFvRegistrationFee(registration);
|
||||
}
|
||||
touched.add(hit.getPsdId());
|
||||
updated++;
|
||||
}
|
||||
if (registration != null)
|
||||
{
|
||||
hit.setFvRegistrationFee(registration);
|
||||
}
|
||||
touched.add(hit.getPsdId());
|
||||
updated++;
|
||||
continue;
|
||||
}
|
||||
if (freight == null && registration == null)
|
||||
{
|
||||
skippedFee++;
|
||||
continue;
|
||||
}
|
||||
PrcPriceSetDetail created = buildDetailFromImportRow(
|
||||
priceSetId, psvId, productCode, productName, row, freight, registration);
|
||||
created.setPspId(pspId);
|
||||
detailMapper.insertPrcPriceSetDetail(created);
|
||||
candidates.add(created);
|
||||
if (created.getPsdId() != null)
|
||||
{
|
||||
touched.add(created.getPsdId());
|
||||
}
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
setProductMapper.bindDetailPspByPsvId(psvId);
|
||||
reportImportProgress(taskId, priceSetId, 95, "finishing");
|
||||
return MessageUtils.message("price.set.import.upsertSuccess",
|
||||
Integer.valueOf(inserted), Integer.valueOf(updated), Integer.valueOf(skippedFee));
|
||||
}
|
||||
|
||||
private String resolveImportProductName(Long psvId, String productCode, List<PrcPriceSetDetail> candidates)
|
||||
{
|
||||
if (candidates != null)
|
||||
{
|
||||
for (PrcPriceSetDetail d : candidates)
|
||||
{
|
||||
if (d != null && StringUtils.isNotEmpty(d.getProductName()))
|
||||
{
|
||||
return d.getProductName();
|
||||
}
|
||||
}
|
||||
}
|
||||
reportImportProgress(taskId, priceSetId, 95, "finishing");
|
||||
return MessageUtils.message("price.set.import.success",
|
||||
Integer.valueOf(updated), Integer.valueOf(unmatched), Integer.valueOf(skippedProduct));
|
||||
if (psvId != null && StringUtils.isNotEmpty(productCode))
|
||||
{
|
||||
PrcPriceSetProduct sp = setProductMapper.selectByPsvIdAndCode(psvId, productCode);
|
||||
if (sp != null && StringUtils.isNotEmpty(sp.getProductName()))
|
||||
{
|
||||
return sp.getProductName();
|
||||
}
|
||||
}
|
||||
BmsProduct lib = productMapper.selectBmsProductByCode(productCode);
|
||||
if (lib != null && StringUtils.isNotEmpty(lib.getProductName()))
|
||||
{
|
||||
return lib.getProductName();
|
||||
}
|
||||
return productCode;
|
||||
}
|
||||
|
||||
private PrcPriceSetDetail buildDetailFromImportRow(Long priceSetId, Long psvId, String productCode,
|
||||
String productName, BmsProductCountry row, BigDecimal freight, BigDecimal registration)
|
||||
{
|
||||
PrcPriceSetDetail d = new PrcPriceSetDetail();
|
||||
d.setPriceSetId(priceSetId);
|
||||
d.setPsvId(psvId);
|
||||
d.setProductCode(productCode);
|
||||
d.setProductName(productName);
|
||||
d.setCountryCode(row.getCountryCode());
|
||||
d.setCountryName(row.getCountryName());
|
||||
d.setZoneId(row.getZoneId());
|
||||
d.setZoneCode(row.getZoneCode());
|
||||
d.setZoneName(row.getZoneName());
|
||||
d.setWeightMin(row.getWeightMin());
|
||||
d.setWeightMax(row.getWeightMax());
|
||||
d.setWeightLabel(formatWeightLabel(row.getWeightMin(), row.getWeightMax()));
|
||||
d.setProductCountryId(row.getDetailId());
|
||||
d.setFvValue(freight);
|
||||
d.setFvRegistrationFee(registration);
|
||||
d.setRoundMode("HALF_UP");
|
||||
d.setEffectiveDate(row.getEffectiveDate());
|
||||
d.setExpireDate(row.getExpireDate());
|
||||
return d;
|
||||
}
|
||||
|
||||
private void reportImportProgress(String taskId, Long priceSetId, int percent, String message)
|
||||
@@ -1097,6 +1259,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
detail.setPsvId(old.getPsvId());
|
||||
}
|
||||
validateDetail(db, detail);
|
||||
Long pspId = lookupSetProductId(detail.getPsvId(), detail.getProductCode());
|
||||
if (pspId == null)
|
||||
{
|
||||
pspId = old.getPspId();
|
||||
}
|
||||
detail.setPspId(pspId);
|
||||
int rows = detailMapper.updatePrcPriceSetDetail(detail);
|
||||
// non-null surcharges = replace snapshot for this detail (empty list clears)
|
||||
if (detail.getSurcharges() != null)
|
||||
@@ -1112,6 +1280,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
@Override
|
||||
public int deleteDetailByIds(Long[] psdIds)
|
||||
{
|
||||
Set<String> touchKeys = new HashSet<String>();
|
||||
for (Long id : psdIds)
|
||||
{
|
||||
PrcPriceSetDetail old = detailMapper.selectPrcPriceSetDetailById(id);
|
||||
@@ -1121,8 +1290,31 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
PrcPriceSet db = requireSet(old.getPriceSetId());
|
||||
assertChildEditable(db);
|
||||
if (old.getPsvId() != null && StringUtils.isNotEmpty(old.getProductCode()))
|
||||
{
|
||||
touchKeys.add(old.getPsvId() + "|" + old.getProductCode());
|
||||
}
|
||||
}
|
||||
return detailMapper.deletePrcPriceSetDetailByIds(psdIds);
|
||||
int rows = detailMapper.deletePrcPriceSetDetailByIds(psdIds);
|
||||
for (String key : touchKeys)
|
||||
{
|
||||
int sep = key.indexOf('|');
|
||||
if (sep <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Long psvId = Long.valueOf(key.substring(0, sep));
|
||||
String productCode = key.substring(sep + 1);
|
||||
PrcPriceSetDetail q = new PrcPriceSetDetail();
|
||||
q.setPsvId(psvId);
|
||||
q.setProductCode(productCode);
|
||||
List<PrcPriceSetDetail> left = detailMapper.selectPrcPriceSetDetailList(q);
|
||||
if (left == null || left.isEmpty())
|
||||
{
|
||||
setProductMapper.deleteByPsvIdAndCode(psvId, productCode);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1818,7 +2010,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
|
||||
private PrcPriceSet requireSet(Long priceSetId)
|
||||
{
|
||||
PrcPriceSet db = setMapper.selectPrcPriceSetById(priceSetId);
|
||||
// Header only: avoid count(detail) waiting on long pull/import locks.
|
||||
PrcPriceSet db = setMapper.selectPrcPriceSetHeaderById(priceSetId);
|
||||
if (db == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.notExists"));
|
||||
@@ -1906,16 +2099,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
set.setVersions(versions);
|
||||
PrcPriceSetVersion current = pickCurrentVersion(versions);
|
||||
set.setVersion(current);
|
||||
if (current != null && current.getDetails() != null)
|
||||
{
|
||||
set.setDetails(current.getDetails());
|
||||
}
|
||||
else
|
||||
{
|
||||
PrcPriceSetDetail query = new PrcPriceSetDetail();
|
||||
query.setPriceSetId(priceSetId);
|
||||
set.setDetails(detailMapper.selectPrcPriceSetDetailList(query));
|
||||
}
|
||||
// Do not load full details here (large sets). Front loads by product via /detail/byProduct.
|
||||
set.setDetails(new ArrayList<PrcPriceSetDetail>());
|
||||
PrcCustomerPriceSet cquery = new PrcCustomerPriceSet();
|
||||
cquery.setPriceSetId(priceSetId);
|
||||
set.setCustomers(customerMapper.selectPrcCustomerPriceSetList(cquery));
|
||||
@@ -1923,41 +2108,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
|
||||
private void fillVersionChildren(PrcPriceSetVersion version)
|
||||
{
|
||||
PrcPriceSetDetail dq = new PrcPriceSetDetail();
|
||||
dq.setPsvId(version.getPsvId());
|
||||
List<PrcPriceSetDetail> details = detailMapper.selectPrcPriceSetDetailList(dq);
|
||||
PrcPriceSetVersionSurcharge sq = new PrcPriceSetVersionSurcharge();
|
||||
sq.setPsvId(version.getPsvId());
|
||||
List<PrcPriceSetVersionSurcharge> surcharges = versionSurchargeMapper.selectPrcPriceSetVersionSurchargeList(sq);
|
||||
if (surcharges == null)
|
||||
{
|
||||
surcharges = new ArrayList<PrcPriceSetVersionSurcharge>();
|
||||
}
|
||||
version.setSurcharges(surcharges);
|
||||
Map<Long, List<PrcPriceSetVersionSurcharge>> byPsd = new HashMap<Long, List<PrcPriceSetVersionSurcharge>>();
|
||||
for (PrcPriceSetVersionSurcharge sc : surcharges)
|
||||
{
|
||||
if (sc.getPsdId() == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<PrcPriceSetVersionSurcharge> list = byPsd.get(sc.getPsdId());
|
||||
if (list == null)
|
||||
{
|
||||
list = new ArrayList<PrcPriceSetVersionSurcharge>();
|
||||
byPsd.put(sc.getPsdId(), list);
|
||||
}
|
||||
list.add(sc);
|
||||
}
|
||||
if (details != null)
|
||||
{
|
||||
for (PrcPriceSetDetail d : details)
|
||||
{
|
||||
List<PrcPriceSetVersionSurcharge> list = byPsd.get(d.getPsdId());
|
||||
d.setSurcharges(list != null ? list : new ArrayList<PrcPriceSetVersionSurcharge>());
|
||||
}
|
||||
}
|
||||
version.setDetails(details);
|
||||
PrcPriceSetProduct pq = new PrcPriceSetProduct();
|
||||
pq.setPsvId(version.getPsvId());
|
||||
List<PrcPriceSetProduct> products = setProductMapper.selectPrcPriceSetProductList(pq);
|
||||
version.setProducts(products == null ? new ArrayList<PrcPriceSetProduct>() : products);
|
||||
// details + surcharge snapshots are loaded on demand (edit price / detail drawer)
|
||||
version.setDetails(new ArrayList<PrcPriceSetDetail>());
|
||||
version.setSurcharges(new ArrayList<PrcPriceSetVersionSurcharge>());
|
||||
}
|
||||
|
||||
private PrcPriceSetVersion pickCurrentVersion(List<PrcPriceSetVersion> versions)
|
||||
@@ -1983,14 +2140,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
// null details = leave existing rows alone; empty list = clear / replace with nothing
|
||||
boolean detailsProvided = set.getDetails() != null;
|
||||
List<PrcPriceSetDetail> details = filterDetails(set.getDetails());
|
||||
// Allow creating an empty draft version when client sends version (Step1 -> Step2).
|
||||
if (version == null && !detailsProvided && !pullLibrary)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (version == null && details.isEmpty() && !pullLibrary)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (version == null)
|
||||
{
|
||||
version = new PrcPriceSetVersion();
|
||||
@@ -2097,18 +2251,21 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
if (existing > 0 && !force)
|
||||
{
|
||||
reportSaveProgress(set, 80, "details exist, skip pull");
|
||||
PrcPriceSetDetail query = new PrcPriceSetDetail();
|
||||
query.setPsvId(version.getPsvId());
|
||||
List<PrcPriceSetDetail> details = detailMapper.selectPrcPriceSetDetailList(query);
|
||||
set.setDetails(details);
|
||||
version.setDetails(details);
|
||||
fillVersionChildren(version);
|
||||
set.setDetails(new ArrayList<PrcPriceSetDetail>());
|
||||
return;
|
||||
}
|
||||
versionSurchargeMapper.deleteByPsvId(version.getPsvId());
|
||||
detailMapper.deletePrcPriceSetDetailByPsvId(version.getPsvId());
|
||||
setProductMapper.deleteByPsvId(version.getPsvId());
|
||||
// clean legacy rows without psv_id under this set (would look like duplicates in UI)
|
||||
detailMapper.deleteOrphanDetailsByPriceSetId(set.getPriceSetId());
|
||||
int productRows = setProductMapper.insertFromLibrary(set.getPriceSetId(), version.getPsvId());
|
||||
if (productRows <= 0)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.library.empty"));
|
||||
}
|
||||
reportSaveProgress(set, 45, "pulling library");
|
||||
int inserted;
|
||||
if (TYPE_DISCOUNT.equals(set.getSetType()))
|
||||
{
|
||||
@@ -2130,15 +2287,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.library.empty"));
|
||||
}
|
||||
setProductMapper.bindDetailPspByPsvId(version.getPsvId());
|
||||
reportSaveProgress(set, 75, "saving details " + inserted + "/" + inserted);
|
||||
reportSaveProgress(set, 85, "saving surcharges");
|
||||
versionSurchargeMapper.insertFromLibraryRels(version.getPsvId(), version.getCreateBy());
|
||||
PrcPriceSetDetail query = new PrcPriceSetDetail();
|
||||
query.setPsvId(version.getPsvId());
|
||||
List<PrcPriceSetDetail> details = detailMapper.selectPrcPriceSetDetailList(query);
|
||||
set.setDetails(details);
|
||||
version.setDetails(details);
|
||||
fillVersionChildren(version);
|
||||
set.setDetails(new ArrayList<PrcPriceSetDetail>());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2171,6 +2325,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
validateDetail(set, d);
|
||||
d.setPriceSetId(set.getPriceSetId());
|
||||
d.setPsvId(version.getPsvId());
|
||||
Long pspId = lookupSetProductId(version.getPsvId(), d.getProductCode());
|
||||
if (pspId != null)
|
||||
{
|
||||
d.setPspId(pspId);
|
||||
}
|
||||
if (d.getPsdId() != null && byId.containsKey(d.getPsdId()))
|
||||
{
|
||||
detailMapper.updatePrcPriceSetDetail(d);
|
||||
@@ -2213,9 +2372,107 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
detailMapper.deletePrcPriceSetDetailByIds(orphanIds.toArray(new Long[0]));
|
||||
}
|
||||
// drop set-product rows whose codes no longer have any detail
|
||||
Set<String> keepCodes = new HashSet<String>();
|
||||
if (details != null)
|
||||
{
|
||||
for (PrcPriceSetDetail d : details)
|
||||
{
|
||||
if (d != null && StringUtils.isNotEmpty(d.getProductCode()))
|
||||
{
|
||||
keepCodes.add(d.getProductCode().trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
PrcPriceSetProduct pq = new PrcPriceSetProduct();
|
||||
pq.setPsvId(version.getPsvId());
|
||||
List<PrcPriceSetProduct> setProducts = setProductMapper.selectPrcPriceSetProductList(pq);
|
||||
if (setProducts != null)
|
||||
{
|
||||
for (PrcPriceSetProduct sp : setProducts)
|
||||
{
|
||||
if (sp == null || StringUtils.isEmpty(sp.getProductCode()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!keepCodes.contains(sp.getProductCode()))
|
||||
{
|
||||
setProductMapper.deleteByPsvIdAndCode(version.getPsvId(), sp.getProductCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
version.setDetails(details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup existing set-product id only (no insert).
|
||||
* Insert happens only on pull-from-library / Excel import.
|
||||
*/
|
||||
private Long lookupSetProductId(Long psvId, String productCode)
|
||||
{
|
||||
if (psvId == null || StringUtils.isEmpty(productCode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PrcPriceSetProduct existing = setProductMapper.selectByPsvIdAndCode(psvId, productCode.trim());
|
||||
return existing == null ? null : existing.getPspId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure package-side product master row exists for this version+code; return psp_id.
|
||||
* Only called from pull-from-library / Excel import.
|
||||
*/
|
||||
private Long ensureSetProduct(Long priceSetId, Long psvId, String productCode, String productNameHint)
|
||||
{
|
||||
if (psvId == null || StringUtils.isEmpty(productCode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String code = productCode.trim();
|
||||
PrcPriceSetProduct existing = setProductMapper.selectByPsvIdAndCode(psvId, code);
|
||||
if (existing != null)
|
||||
{
|
||||
if (StringUtils.isEmpty(existing.getProductName()) && StringUtils.isNotEmpty(productNameHint)
|
||||
&& !code.equals(productNameHint))
|
||||
{
|
||||
existing.setProductName(productNameHint);
|
||||
setProductMapper.updatePrcPriceSetProduct(existing);
|
||||
}
|
||||
return existing.getPspId();
|
||||
}
|
||||
PrcPriceSetProduct row = new PrcPriceSetProduct();
|
||||
row.setPriceSetId(priceSetId);
|
||||
row.setPsvId(psvId);
|
||||
row.setProductCode(code);
|
||||
BmsProduct lib = null;
|
||||
try
|
||||
{
|
||||
lib = productMapper.selectBmsProductByCode(code);
|
||||
}
|
||||
catch (Exception ignore)
|
||||
{
|
||||
lib = null;
|
||||
}
|
||||
if (lib != null)
|
||||
{
|
||||
row.setProductName(lib.getProductName());
|
||||
row.setServiceLevel(lib.getServiceLevel());
|
||||
row.setCargoAttr(lib.getCargoAttr());
|
||||
row.setDestRegion(lib.getDestRegion());
|
||||
row.setUpdateNote(lib.getUpdateNote());
|
||||
row.setTaxMode(StringUtils.isNotEmpty(lib.getTaxMode()) ? lib.getTaxMode() : "TAX_INCLUDED");
|
||||
row.setSourceProductId(lib.getProductId());
|
||||
row.setSourceSheet(lib.getSourceSheet());
|
||||
}
|
||||
else
|
||||
{
|
||||
row.setProductName(StringUtils.isNotEmpty(productNameHint) ? productNameHint : code);
|
||||
row.setTaxMode("TAX_INCLUDED");
|
||||
}
|
||||
setProductMapper.insertPrcPriceSetProduct(row);
|
||||
return row.getPspId();
|
||||
}
|
||||
|
||||
/** Only rewrite surcharge snapshots for rows that explicitly sent surcharges. */
|
||||
private void resnapshotSurchargesIfNeeded(PrcPriceSetVersion version, List<PrcPriceSetDetail> details)
|
||||
{
|
||||
|
||||
+99
-8
@@ -28,6 +28,7 @@ import com.bms.price.domain.BmsProductCountry;
|
||||
import com.bms.price.domain.PrcCustomerPriceSet;
|
||||
import com.bms.price.domain.PrcPriceSet;
|
||||
import com.bms.price.domain.PrcPriceSetDetail;
|
||||
import com.bms.price.domain.PrcPriceSetProduct;
|
||||
import com.bms.price.domain.PrcPriceSetVersion;
|
||||
import com.bms.price.domain.PrcPriceSetVersionSurcharge;
|
||||
import com.bms.price.domain.PrcQuote;
|
||||
@@ -45,6 +46,7 @@ import com.bms.price.mapper.BmsProductMapper;
|
||||
import com.bms.price.mapper.PrcCustomerPriceSetMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetDetailMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetProductMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetVersionMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetVersionSurchargeMapper;
|
||||
import com.bms.price.mapper.PrcQuoteFeeMapper;
|
||||
@@ -90,6 +92,9 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
@Autowired
|
||||
private PrcPriceSetDetailMapper detailMapper;
|
||||
|
||||
@Autowired
|
||||
private PrcPriceSetProductMapper setProductMapper;
|
||||
|
||||
@Autowired
|
||||
private BmsProductCountryMapper productCountryMapper;
|
||||
|
||||
@@ -195,6 +200,98 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return toResult(quote, quoteFeeMapper.selectByQuoteId(quoteId), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PrcPriceSetProduct> listBoundPackageProducts(String customerCode, Date bizDate)
|
||||
{
|
||||
if (StringUtils.isEmpty(customerCode))
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<PrcCustomerPriceSet> binds = customerPriceSetMapper.selectEffectiveByCustomerCode(customerCode.trim());
|
||||
if (binds == null || binds.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Date day = bizDate != null ? bizDate : new Date();
|
||||
Date asOf = endOfDay(day);
|
||||
for (PrcCustomerPriceSet bind : binds)
|
||||
{
|
||||
if (bind == null || bind.getPriceSetId() == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
PrcPriceSet set = priceSetMapper.selectPrcPriceSetHeaderById(bind.getPriceSetId());
|
||||
if (set == null || !"E".equals(set.getStatus()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<PrcPriceSetProduct> products = loadProductsForPackage(set.getPriceSetId(), asOf);
|
||||
if (products != null && !products.isEmpty())
|
||||
{
|
||||
return products;
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/** Prefer version active at asOf; else latest version that has set-product rows. */
|
||||
private List<PrcPriceSetProduct> loadProductsForPackage(Long priceSetId, Date asOf)
|
||||
{
|
||||
PrcPriceSetVersion active = versionMapper.selectActiveByPriceSetAt(priceSetId, asOf);
|
||||
List<PrcPriceSetProduct> rows = listProductsByPsvId(active == null ? null : active.getPsvId());
|
||||
if (rows != null && !rows.isEmpty())
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
PrcPriceSetVersion q = new PrcPriceSetVersion();
|
||||
q.setPriceSetId(priceSetId);
|
||||
List<PrcPriceSetVersion> versions = versionMapper.selectPrcPriceSetVersionList(q);
|
||||
if (versions == null || versions.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// newest seq first
|
||||
versions.sort((a, b) -> {
|
||||
int sa = a.getVersionSeq() == null ? 0 : a.getVersionSeq().intValue();
|
||||
int sb = b.getVersionSeq() == null ? 0 : b.getVersionSeq().intValue();
|
||||
return Integer.compare(sb, sa);
|
||||
});
|
||||
for (PrcPriceSetVersion v : versions)
|
||||
{
|
||||
rows = listProductsByPsvId(v == null ? null : v.getPsvId());
|
||||
if (rows != null && !rows.isEmpty())
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private List<PrcPriceSetProduct> listProductsByPsvId(Long psvId)
|
||||
{
|
||||
if (psvId == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
PrcPriceSetProduct q = new PrcPriceSetProduct();
|
||||
q.setPsvId(psvId);
|
||||
List<PrcPriceSetProduct> rows = setProductMapper.selectPrcPriceSetProductList(q);
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Map<String, PrcPriceSetProduct> byCode = new LinkedHashMap<String, PrcPriceSetProduct>();
|
||||
for (PrcPriceSetProduct row : rows)
|
||||
{
|
||||
if (row == null || StringUtils.isEmpty(row.getProductCode()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
byCode.putIfAbsent(row.getProductCode(), row);
|
||||
}
|
||||
return new ArrayList<PrcPriceSetProduct>(byCode.values());
|
||||
}
|
||||
|
||||
private TmsPriceQuoteResult doQuote(TmsPriceQuoteRequest request, Date bizDate)
|
||||
{
|
||||
String productCode = request.getProductCode().trim();
|
||||
@@ -485,14 +582,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
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;
|
||||
// FIXED sales price is version-detail only; no fallback to product_country.
|
||||
return null;
|
||||
}
|
||||
|
||||
PrcPriceSetDetail detail = detailMapper.selectFixedMatch(
|
||||
|
||||
@@ -100,6 +100,29 @@ public class PrcPriceSetSaveAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Background pull of product library into package version details. */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runPullLibrary(String taskId, Long priceSetId, Long psvId, boolean force, String operName)
|
||||
{
|
||||
try
|
||||
{
|
||||
put(taskId, PrcPriceSetSaveTask.STATUS_RUNNING, 10, "pulling library", priceSetId, null);
|
||||
String message = priceSetService.pullFromLibrary(priceSetId, psvId, force, operName, taskId);
|
||||
put(taskId, PrcPriceSetSaveTask.STATUS_SUCCESS, 100, message, priceSetId, null);
|
||||
log.info("async pull library done, taskId={}, priceSetId={}", taskId, priceSetId);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("async pull library failed, taskId={}, priceSetId={}", taskId, priceSetId, e);
|
||||
String msg = e.getMessage();
|
||||
if (StringUtils.isEmpty(msg))
|
||||
{
|
||||
msg = e.getClass().getSimpleName();
|
||||
}
|
||||
put(taskId, PrcPriceSetSaveTask.STATUS_FAIL, 100, "failed", priceSetId, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void put(String taskId, String status, int percent, String message, Long priceSetId, String error)
|
||||
{
|
||||
PrcPriceSetSaveTask task = new PrcPriceSetSaveTask();
|
||||
|
||||
@@ -8,6 +8,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<id property="psdId" column="psd_id" />
|
||||
<result property="priceSetId" column="price_set_id" />
|
||||
<result property="psvId" column="psv_id" />
|
||||
<result property="pspId" column="psp_id" />
|
||||
<result property="productCode" column="product_code" />
|
||||
<result property="productName" column="product_name" />
|
||||
<result property="countryCode" column="country_code" />
|
||||
@@ -35,7 +36,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectPrcPriceSetDetailVo">
|
||||
select psd_id, price_set_id, psv_id, product_code, product_name, country_code, country_name,
|
||||
select psd_id, price_set_id, psv_id, psp_id, product_code, product_name, country_code, country_name,
|
||||
zone_id, zone_code, zone_name,
|
||||
zd_id, wsd_id, weight_label, weight_min, weight_max, product_country_id, fv_value, fv_registration_fee,
|
||||
discount_value, round_mode, pr_id, effective_date, expire_date, create_time, update_time
|
||||
@@ -48,7 +49,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</select>
|
||||
|
||||
<select id="selectPrcPriceSetDetailList" parameterType="PrcPriceSetDetail" resultMap="PrcPriceSetDetailResult">
|
||||
select d.psd_id, d.price_set_id, d.psv_id, d.product_code, d.product_name, d.country_code, d.country_name,
|
||||
select d.psd_id, d.price_set_id, d.psv_id, d.psp_id, d.product_code, d.product_name, d.country_code, d.country_name,
|
||||
d.zone_id, d.zone_code, d.zone_name,
|
||||
d.zd_id, d.wsd_id, d.weight_label, d.weight_min, d.weight_max, d.product_country_id,
|
||||
d.fv_value, d.fv_registration_fee,
|
||||
@@ -210,6 +211,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
insert into prc_price_set_detail (
|
||||
<if test="priceSetId != null">price_set_id,</if>
|
||||
<if test="psvId != null">psv_id,</if>
|
||||
<if test="pspId != null">psp_id,</if>
|
||||
<if test="productCode != null and productCode != ''">product_code,</if>
|
||||
<if test="productName != null">product_name,</if>
|
||||
<if test="countryCode != null">country_code,</if>
|
||||
@@ -234,6 +236,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
) values (
|
||||
<if test="priceSetId != null">#{priceSetId},</if>
|
||||
<if test="psvId != null">#{psvId},</if>
|
||||
<if test="pspId != null">#{pspId},</if>
|
||||
<if test="productCode != null and productCode != ''">#{productCode},</if>
|
||||
<if test="productName != null">#{productName},</if>
|
||||
<if test="countryCode != null">#{countryCode},</if>
|
||||
@@ -273,6 +276,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
update prc_price_set_detail
|
||||
<set>
|
||||
<if test="psvId != null">psv_id = #{psvId},</if>
|
||||
<if test="pspId != null">psp_id = #{pspId},</if>
|
||||
<if test="productCode != null and productCode != ''">product_code = #{productCode},</if>
|
||||
<if test="productName != null">product_name = #{productName},</if>
|
||||
country_code = #{countryCode},
|
||||
|
||||
@@ -28,7 +28,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
s.set_type, s.default_discount, s.default_type, s.calculate_sign,
|
||||
s.status, s.tms_id,
|
||||
s.created_by, s.create_time, s.updated_by, s.update_time,
|
||||
(select count(distinct d.product_code) from prc_price_set_detail d where d.price_set_id = s.price_set_id) as product_count,
|
||||
(select count(distinct p.product_code) from prc_price_set_product p where p.price_set_id = s.price_set_id) as product_count,
|
||||
(select count(1) from prc_customer_price_set c where c.price_set_id = s.price_set_id) as customer_count
|
||||
from prc_price_set s
|
||||
</sql>
|
||||
@@ -38,6 +38,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where s.price_set_id = #{priceSetId}
|
||||
</select>
|
||||
|
||||
<select id="selectPrcPriceSetHeaderById" parameterType="Long" resultMap="PrcPriceSetResult">
|
||||
select s.price_set_id, s.price_set_code, s.price_set_name, s.price_set_note,
|
||||
s.set_type, s.default_discount, s.default_type, s.calculate_sign,
|
||||
s.status, s.tms_id,
|
||||
s.created_by, s.create_time, s.updated_by, s.update_time,
|
||||
null as product_count, null as customer_count
|
||||
from prc_price_set s
|
||||
where s.price_set_id = #{priceSetId}
|
||||
</select>
|
||||
|
||||
<select id="selectPrcPriceSetList" parameterType="PrcPriceSet" resultMap="PrcPriceSetResult">
|
||||
<include refid="selectPrcPriceSetVo"/>
|
||||
<where>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.bms.price.mapper.PrcPriceSetProductMapper">
|
||||
|
||||
<resultMap type="PrcPriceSetProduct" id="PrcPriceSetProductResult">
|
||||
<id property="pspId" column="psp_id" />
|
||||
<result property="priceSetId" column="price_set_id" />
|
||||
<result property="psvId" column="psv_id" />
|
||||
<result property="productCode" column="product_code" />
|
||||
<result property="productName" column="product_name" />
|
||||
<result property="serviceLevel" column="service_level" />
|
||||
<result property="cargoAttr" column="cargo_attr" />
|
||||
<result property="destRegion" column="dest_region" />
|
||||
<result property="updateNote" column="update_note" />
|
||||
<result property="taxMode" column="tax_mode" />
|
||||
<result property="sourceProductId" column="source_product_id" />
|
||||
<result property="sourceSheet" column="source_sheet" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectVo">
|
||||
select psp_id, price_set_id, psv_id, product_code, product_name, service_level, cargo_attr,
|
||||
dest_region, update_note, tax_mode, source_product_id, source_sheet, create_time, update_time
|
||||
from prc_price_set_product
|
||||
</sql>
|
||||
|
||||
<select id="selectPrcPriceSetProductById" parameterType="Long" resultMap="PrcPriceSetProductResult">
|
||||
<include refid="selectVo"/>
|
||||
where psp_id = #{pspId}
|
||||
</select>
|
||||
|
||||
<select id="selectPrcPriceSetProductList" parameterType="PrcPriceSetProduct" resultMap="PrcPriceSetProductResult">
|
||||
<include refid="selectVo"/>
|
||||
<where>
|
||||
<if test="priceSetId != null">AND price_set_id = #{priceSetId}</if>
|
||||
<if test="psvId != null">AND psv_id = #{psvId}</if>
|
||||
<if test="productCode != null and productCode != ''">AND product_code = #{productCode}</if>
|
||||
</where>
|
||||
order by product_code asc, psp_id asc
|
||||
</select>
|
||||
|
||||
<select id="selectByPsvIdAndCode" resultMap="PrcPriceSetProductResult">
|
||||
<include refid="selectVo"/>
|
||||
where psv_id = #{psvId}
|
||||
and product_code = #{productCode}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<insert id="insertPrcPriceSetProduct" parameterType="PrcPriceSetProduct" useGeneratedKeys="true" keyProperty="pspId">
|
||||
insert into prc_price_set_product (
|
||||
price_set_id, psv_id, product_code, product_name, service_level, cargo_attr,
|
||||
dest_region, update_note, tax_mode, source_product_id, source_sheet, create_time
|
||||
) values (
|
||||
#{priceSetId}, #{psvId}, #{productCode}, #{productName}, #{serviceLevel}, #{cargoAttr},
|
||||
#{destRegion}, #{updateNote}, #{taxMode}, #{sourceProductId}, #{sourceSheet}, sysdate()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updatePrcPriceSetProduct" parameterType="PrcPriceSetProduct">
|
||||
update prc_price_set_product
|
||||
<set>
|
||||
<if test="productName != null">product_name = #{productName},</if>
|
||||
<if test="serviceLevel != null">service_level = #{serviceLevel},</if>
|
||||
<if test="cargoAttr != null">cargo_attr = #{cargoAttr},</if>
|
||||
<if test="destRegion != null">dest_region = #{destRegion},</if>
|
||||
<if test="updateNote != null">update_note = #{updateNote},</if>
|
||||
<if test="taxMode != null and taxMode != ''">tax_mode = #{taxMode},</if>
|
||||
<if test="sourceProductId != null">source_product_id = #{sourceProductId},</if>
|
||||
<if test="sourceSheet != null">source_sheet = #{sourceSheet},</if>
|
||||
update_time = sysdate()
|
||||
</set>
|
||||
where psp_id = #{pspId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteByPsvId">
|
||||
delete from prc_price_set_product where psv_id = #{psvId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByPsvIdAndCode">
|
||||
delete from prc_price_set_product
|
||||
where psv_id = #{psvId}
|
||||
and product_code = #{productCode}
|
||||
</delete>
|
||||
|
||||
<insert id="copyFromVersion">
|
||||
insert into prc_price_set_product (
|
||||
price_set_id, psv_id, product_code, product_name, service_level, cargo_attr,
|
||||
dest_region, update_note, tax_mode, source_product_id, source_sheet, create_time
|
||||
)
|
||||
select
|
||||
price_set_id, #{newPsvId}, product_code, product_name, service_level, cargo_attr,
|
||||
dest_region, update_note, tax_mode, source_product_id, source_sheet, sysdate()
|
||||
from prc_price_set_product
|
||||
where psv_id = #{srcPsvId}
|
||||
order by psp_id asc
|
||||
</insert>
|
||||
|
||||
<insert id="insertFromLibrary">
|
||||
insert ignore into prc_price_set_product (
|
||||
price_set_id, psv_id, product_code, product_name, service_level, cargo_attr,
|
||||
dest_region, update_note, tax_mode, source_product_id, source_sheet, create_time
|
||||
)
|
||||
select
|
||||
#{priceSetId},
|
||||
#{psvId},
|
||||
p.product_code,
|
||||
p.product_name,
|
||||
p.service_level,
|
||||
p.cargo_attr,
|
||||
p.dest_region,
|
||||
p.update_note,
|
||||
ifnull(p.tax_mode, 'TAX_INCLUDED'),
|
||||
p.product_id,
|
||||
p.source_sheet,
|
||||
sysdate()
|
||||
from product p
|
||||
where p.status = '1'
|
||||
and p.product_id = (
|
||||
select max(p2.product_id) from product p2
|
||||
where p2.product_code = p.product_code and p2.status = '1'
|
||||
)
|
||||
order by p.product_code asc
|
||||
</insert>
|
||||
|
||||
<update id="bindDetailPspByPsvId">
|
||||
update prc_price_set_detail d
|
||||
inner join prc_price_set_product sp
|
||||
on sp.psv_id = d.psv_id and sp.product_code = d.product_code
|
||||
set d.psp_id = sp.psp_id
|
||||
where d.psv_id = #{psvId}
|
||||
and (d.psp_id is null or d.psp_id <> sp.psp_id)
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -1257,6 +1257,7 @@ CREATE TABLE `prc_price_set_detail` (
|
||||
`psd_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '套餐明细id',
|
||||
`price_set_id` bigint(20) NOT NULL COMMENT '价格套餐id',
|
||||
`psv_id` bigint(20) DEFAULT NULL COMMENT '套餐版本id',
|
||||
`psp_id` bigint(20) DEFAULT NULL COMMENT '套餐产品主表id prc_price_set_product.psp_id',
|
||||
`product_code` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品代码',
|
||||
`product_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品名称',
|
||||
`country_code` char(2) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '国家二字码',
|
||||
@@ -1286,6 +1287,7 @@ CREATE TABLE `prc_price_set_detail` (
|
||||
KEY `idx_psd_zone` (`zone_id`),
|
||||
KEY `idx_psd_country_zone` (`price_set_id`,`product_code`,`country_code`,`zone_code`),
|
||||
KEY `idx_psd_psv` (`psv_id`),
|
||||
KEY `idx_psd_psp` (`psp_id`),
|
||||
KEY `idx_psd_weight_match` (`price_set_id`,`product_code`,`country_code`,`weight_min`,`weight_max`),
|
||||
KEY `idx_psd_eff` (`price_set_id`,`effective_date`,`expire_date`),
|
||||
CONSTRAINT `fk_psd_psv` FOREIGN KEY (`psv_id`) REFERENCES `prc_price_set_version` (`psv_id`) ON DELETE CASCADE,
|
||||
@@ -1315,6 +1317,33 @@ CREATE TABLE `prc_price_set_version` (
|
||||
CONSTRAINT `fk_psv_set` FOREIGN KEY (`price_set_id`) REFERENCES `prc_price_set` (`price_set_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='价格套餐版本';
|
||||
|
||||
-- ----------------------------
|
||||
-- 表结构:`prc_price_set_product`
|
||||
-- ----------------------------
|
||||
CREATE TABLE `prc_price_set_product` (
|
||||
`psp_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '套餐产品主表id',
|
||||
`price_set_id` bigint(20) NOT NULL COMMENT '价格套餐id',
|
||||
`psv_id` bigint(20) NOT NULL COMMENT '套餐版本id',
|
||||
`product_code` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品对接代码',
|
||||
`product_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品名称',
|
||||
`service_level` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '时效等级',
|
||||
`cargo_attr` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '走货属性',
|
||||
`dest_region` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目的区域',
|
||||
`update_note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新说明',
|
||||
`tax_mode` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'TAX_INCLUDED' COMMENT 'TAX_INCLUDED包税 TAX_EXCLUDED不包税',
|
||||
`source_product_id` bigint(20) DEFAULT NULL COMMENT '来源产品库 product.product_id',
|
||||
`source_sheet` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '来源Excel sheet',
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`psp_id`),
|
||||
UNIQUE KEY `uk_psp_psv_code` (`psv_id`,`product_code`),
|
||||
KEY `idx_psp_set` (`price_set_id`),
|
||||
KEY `idx_psp_psv` (`psv_id`),
|
||||
KEY `idx_psp_code` (`product_code`),
|
||||
CONSTRAINT `fk_psp_psv` FOREIGN KEY (`psv_id`) REFERENCES `prc_price_set_version` (`psv_id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_psp_set` FOREIGN KEY (`price_set_id`) REFERENCES `prc_price_set` (`price_set_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='价格套餐产品主表-与产品库快照独立';
|
||||
|
||||
-- ----------------------------
|
||||
-- 表结构:`prc_price_set_version_surcharge`
|
||||
-- ----------------------------
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
-- prc_price_set_product: package-side product master (snapshot like product)
|
||||
-- + prc_price_set_detail.psp_id FK (run once on existing DB)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `prc_price_set_product` (
|
||||
`psp_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '套餐产品主表id',
|
||||
`price_set_id` bigint(20) NOT NULL COMMENT '价格套餐id',
|
||||
`psv_id` bigint(20) NOT NULL COMMENT '套餐版本id',
|
||||
`product_code` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品对接代码',
|
||||
`product_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品名称',
|
||||
`service_level` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '时效等级',
|
||||
`cargo_attr` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '走货属性',
|
||||
`dest_region` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目的区域',
|
||||
`update_note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新说明',
|
||||
`tax_mode` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'TAX_INCLUDED' COMMENT 'TAX_INCLUDED包税 TAX_EXCLUDED不包税',
|
||||
`source_product_id` bigint(20) DEFAULT NULL COMMENT '来源产品库 product.product_id',
|
||||
`source_sheet` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '来源Excel sheet',
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`psp_id`),
|
||||
UNIQUE KEY `uk_psp_psv_code` (`psv_id`,`product_code`),
|
||||
KEY `idx_psp_set` (`price_set_id`),
|
||||
KEY `idx_psp_psv` (`psv_id`),
|
||||
KEY `idx_psp_code` (`product_code`),
|
||||
CONSTRAINT `fk_psp_psv` FOREIGN KEY (`psv_id`) REFERENCES `prc_price_set_version` (`psv_id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_psp_set` FOREIGN KEY (`price_set_id`) REFERENCES `prc_price_set` (`price_set_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='价格套餐产品主表-与产品库快照独立';
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'prc_price_set_detail' AND COLUMN_NAME = 'psp_id'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
CONCAT(
|
||||
'ALTER TABLE `prc_price_set_detail` ',
|
||||
'ADD COLUMN `psp_id` bigint(20) DEFAULT NULL COMMENT ''套餐产品主表id prc_price_set_product.psp_id'' AFTER `psv_id`, ',
|
||||
'ADD KEY `idx_psd_psp` (`psp_id`)'
|
||||
),
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -9,6 +9,15 @@ export function quoteBilling(data) {
|
||||
})
|
||||
}
|
||||
|
||||
/** Products from customer's bound effective package (trial dropdown) */
|
||||
export function listQuoteBoundProducts(customerCode, bizDate) {
|
||||
return request({
|
||||
url: '/price/billing/quote/products',
|
||||
method: 'get',
|
||||
params: { customerCode, bizDate }
|
||||
})
|
||||
}
|
||||
|
||||
/** Historical quote list */
|
||||
export function listQuote(query) {
|
||||
return request({
|
||||
|
||||
@@ -172,6 +172,15 @@ export function listSetDetail(query) {
|
||||
})
|
||||
}
|
||||
|
||||
/** All detail rows for one product under a version (no pagination). */
|
||||
export function listSetDetailByProduct(psvId, productCode) {
|
||||
return request({
|
||||
url: '/price/set/detail/byProduct',
|
||||
method: 'get',
|
||||
params: { psvId, productCode }
|
||||
})
|
||||
}
|
||||
|
||||
/** 版本明细搜索下拉选项:按版本去重后的 产品/国家/分区 */
|
||||
export function listSetDetailOptions(psvId) {
|
||||
return request({
|
||||
@@ -213,6 +222,31 @@ export async function importSetDetailPricesAsync(file, query, onProgress) {
|
||||
if (!taskId) {
|
||||
throw new Error('missing import taskId')
|
||||
}
|
||||
return pollPriceSetSaveTask(taskId, onProgress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Async pull product library into package version details; poll until done.
|
||||
* @param {{ priceSetId: string|number, psvId?: string|number, force?: boolean }} query
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
*/
|
||||
export async function pullSetDetailsFromLibraryAsync(query, onProgress) {
|
||||
const params = new URLSearchParams()
|
||||
if (query && query.priceSetId != null) params.set('priceSetId', query.priceSetId)
|
||||
if (query && query.psvId != null && query.psvId !== '') params.set('psvId', query.psvId)
|
||||
params.set('force', query && query.force ? 'true' : 'false')
|
||||
const submitRes = await request({
|
||||
url: '/price/set/detail/pullFromLibrary?' + params.toString(),
|
||||
method: 'post'
|
||||
})
|
||||
const taskId = pickSaveTaskId(submitRes)
|
||||
if (!taskId) {
|
||||
throw new Error('missing pull taskId')
|
||||
}
|
||||
return pollPriceSetSaveTask(taskId, onProgress)
|
||||
}
|
||||
|
||||
async function pollPriceSetSaveTask(taskId, onProgress) {
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
@@ -225,13 +259,13 @@ export async function importSetDetailPricesAsync(file, query, onProgress) {
|
||||
return { task }
|
||||
}
|
||||
if (task.status === 'FAIL') {
|
||||
const err = new Error(task.error || task.message || 'import failed')
|
||||
const err = new Error(task.error || task.message || 'task failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
}
|
||||
throw new Error('import timeout')
|
||||
throw new Error('task timeout')
|
||||
}
|
||||
|
||||
export function updateSetDetail(data) {
|
||||
|
||||
@@ -6,22 +6,24 @@
|
||||
<template #header><span>试算参数</span></template>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="客户" prop="customerCode">
|
||||
<customer-select v-model="form.customerCode" value-key="customerCode" placeholder="请选择客户" />
|
||||
<div class="form-hint">与套餐绑定客户一致;未绑定套餐时报错</div>
|
||||
<customer-select v-model="form.customerCode" value-key="customerCode" placeholder="请选择客户" @change="handleCustomerChange" />
|
||||
<div class="form-hint">产品列表来自该客户绑定套餐;未绑定或套餐无产品时为空</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品" prop="productCode">
|
||||
<el-select
|
||||
v-model="form.productCode"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择产品"
|
||||
:placeholder="productPlaceholder"
|
||||
style="width: 100%"
|
||||
:disabled="!form.customerCode"
|
||||
:loading="productLoading"
|
||||
@change="handleProductChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in productOptions"
|
||||
:key="item.productCode"
|
||||
:label="item.productName + ' (' + item.productCode + ')'"
|
||||
:label="(item.productName || item.productCode) + ' (' + item.productCode + ')'"
|
||||
:value="item.productCode"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -262,8 +264,8 @@
|
||||
</template>
|
||||
|
||||
<script setup name="BillingQuote">
|
||||
import { quoteBilling, listQuote, getQuote } from '@/api/price/billing'
|
||||
import { listProduct, listProductCountries } from '@/api/price/product'
|
||||
import { quoteBilling, listQuoteBoundProducts, listQuote, getQuote } from '@/api/price/billing'
|
||||
import { listProductCountries } from '@/api/price/product'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
|
||||
@@ -346,12 +348,62 @@ const rules = {
|
||||
weight: [{ required: true, message: '请输入计费重量', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
function loadProducts() {
|
||||
listProduct({ pageNum: 1, pageSize: 500, status: '1' }).then(res => {
|
||||
productOptions.value = res.rows || []
|
||||
const productLoading = ref(false)
|
||||
const productPlaceholder = computed(() => {
|
||||
if (!form.customerCode) return '请先选择客户'
|
||||
if (productLoading.value) return '加载中…'
|
||||
if (!(productOptions.value || []).length) return '该客户绑定套餐暂无产品'
|
||||
return '请选择产品'
|
||||
})
|
||||
|
||||
function clearProductCascade() {
|
||||
form.productCode = undefined
|
||||
form.countryCode = undefined
|
||||
form.destPostcode = undefined
|
||||
countryOptions.value = []
|
||||
countryRows.value = []
|
||||
productOptions.value = []
|
||||
}
|
||||
|
||||
function handleCustomerChange(code) {
|
||||
clearProductCascade()
|
||||
result.value = null
|
||||
errorMsg.value = ''
|
||||
if (!code) return
|
||||
loadProductsByCustomer(code)
|
||||
}
|
||||
|
||||
function loadProductsByCustomer(customerCode) {
|
||||
if (!customerCode) {
|
||||
productOptions.value = []
|
||||
return Promise.resolve([])
|
||||
}
|
||||
productLoading.value = true
|
||||
return listQuoteBoundProducts(customerCode, form.bizDate || undefined).then(res => {
|
||||
productOptions.value = res.data || []
|
||||
return productOptions.value
|
||||
}).catch(() => {
|
||||
productOptions.value = []
|
||||
return []
|
||||
}).finally(() => {
|
||||
productLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => form.bizDate, () => {
|
||||
if (!form.customerCode) return
|
||||
const keep = form.productCode
|
||||
loadProductsByCustomer(form.customerCode).then(list => {
|
||||
if (keep && !(list || []).some(p => p.productCode === keep)) {
|
||||
form.productCode = undefined
|
||||
form.countryCode = undefined
|
||||
form.destPostcode = undefined
|
||||
countryOptions.value = []
|
||||
countryRows.value = []
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function uniqueCountries(list) {
|
||||
const map = new Map()
|
||||
;(list || []).forEach(item => {
|
||||
@@ -430,6 +482,7 @@ function resetForm() {
|
||||
form.destPostcode = undefined
|
||||
form.weight = undefined
|
||||
form.bizDate = undefined
|
||||
productOptions.value = []
|
||||
countryOptions.value = []
|
||||
countryRows.value = []
|
||||
result.value = null
|
||||
@@ -539,7 +592,6 @@ function sourceTagType(source) {
|
||||
return ''
|
||||
}
|
||||
|
||||
loadProducts()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
border
|
||||
size="small"
|
||||
:span-method="spanMethod"
|
||||
:row-class-name="rowClass"
|
||||
>
|
||||
<el-table-column :label="colCountry" min-width="100">
|
||||
<template #default="{ row }">{{ countryLabel(row) }}</template>
|
||||
@@ -27,16 +26,6 @@
|
||||
<template #default="{ row }">{{ row.zoneName || row.zoneCode || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colWeight" prop="weightLabel" min-width="120" />
|
||||
<el-table-column :label="colBaseFv" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="price-text base-price">{{ formatNum(row._baseFreight, 4) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colBaseReg" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="price-text base-price">{{ formatNum(row._baseReg, 2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colSetFv" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="price-cell">
|
||||
@@ -99,7 +88,6 @@
|
||||
|
||||
<script setup name="PriceEditDrawer">
|
||||
import SurchargeCountCell from './SurchargeCountCell.vue'
|
||||
import { isDetailPriceChanged } from '../../utils/priceSet'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
@@ -109,8 +97,6 @@ const props = defineProps({
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'pick-surcharge', 'row-change', 'surcharge-change'])
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const keyword = ref('')
|
||||
const editingKey = ref('')
|
||||
const committing = ref(false)
|
||||
@@ -121,13 +107,11 @@ const title = computed(() => {
|
||||
const name = props.productName || ''
|
||||
return code + ' · ' + name + ' · 国家服务价格'
|
||||
})
|
||||
const hint = '黄色行=已改价(与产品基础价不同);点编辑图标后修改,失焦即刻写入数据库。单位:运费 RMB/KG,挂号费 RMB/票。'
|
||||
const hint = '点编辑图标后修改,失焦即刻写入数据库。单位:运费 RMB/KG,挂号费 RMB/票。'
|
||||
const phSearch = '搜索国家/分区'
|
||||
const colCountry = '国家'
|
||||
const colZone = '分区'
|
||||
const colWeight = '重量段'
|
||||
const colBaseFv = '产品基础运费'
|
||||
const colBaseReg = '产品基础挂号费'
|
||||
const colSetFv = '套餐运费'
|
||||
const colSetReg = '套餐挂号费'
|
||||
const colSurcharge = '附加费'
|
||||
@@ -159,20 +143,6 @@ function startEdit(row, field) {
|
||||
})
|
||||
}
|
||||
|
||||
function isBelowFloor(value, floor) {
|
||||
if (value == null || value === '' || floor == null || floor === '') return false
|
||||
const n = Number(value)
|
||||
const f = Number(floor)
|
||||
if (Number.isNaN(n) || Number.isNaN(f)) return false
|
||||
return n + 1e-8 < f
|
||||
}
|
||||
|
||||
function belowFloorMessage(field, floor) {
|
||||
const name = field === 'fv' ? '\u5957\u9910\u8fd0\u8d39' : '\u5957\u9910\u6302\u53f7\u8d39'
|
||||
const prec = field === 'fv' ? 4 : 2
|
||||
return name + '\u4e0d\u80fd\u4f4e\u4e8e\u5bfc\u5165\u57fa\u51c6\u4ef7 ' + formatNum(floor, prec)
|
||||
}
|
||||
|
||||
async function commitEdit(row, field) {
|
||||
if (committing.value) return
|
||||
if (editingKey.value !== cellKey(row, field)) return
|
||||
@@ -180,16 +150,6 @@ async function commitEdit(row, field) {
|
||||
editingKey.value = ''
|
||||
try {
|
||||
if (!row) return
|
||||
// Temporarily allow freight / registration below imported base price.
|
||||
// if (field === 'fv') {
|
||||
// const floor = row._floorFreight
|
||||
// const value = row.fvValue
|
||||
// if (isBelowFloor(value, floor)) {
|
||||
// row.fvValue = row._editPrevFv
|
||||
// proxy.$modal.msgError(belowFloorMessage(field, floor))
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
if (!row.psdId) return
|
||||
emit('row-change', row)
|
||||
} finally {
|
||||
@@ -271,10 +231,6 @@ function spanOf(spans, rowIndex) {
|
||||
return rowspan > 0 ? { rowspan, colspan: 1 } : { rowspan: 0, colspan: 0 }
|
||||
}
|
||||
|
||||
function rowClass({ row }) {
|
||||
return isDetailPriceChanged(row) ? 'changed-row' : ''
|
||||
}
|
||||
|
||||
function close() {
|
||||
editingKey.value = ''
|
||||
emit('update:modelValue', false)
|
||||
@@ -345,13 +301,7 @@ watch(() => props.modelValue, v => {
|
||||
min-width: 64px;
|
||||
text-align: right;
|
||||
}
|
||||
.base-price {
|
||||
color: #909399;
|
||||
}
|
||||
.edit-ico {
|
||||
padding: 0 2px !important;
|
||||
}
|
||||
:deep(.changed-row > td) {
|
||||
background: #fffbe6 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<step-details
|
||||
v-if="loaded"
|
||||
:details="details"
|
||||
:products="products"
|
||||
:price-set-id="priceSetId"
|
||||
:psv-id="version.psvId"
|
||||
:set-type="setType"
|
||||
@@ -34,7 +35,7 @@
|
||||
<script setup name="PriceEditPanel">
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import StepDetails from './StepDetails.vue'
|
||||
import { usePriceSetRefData, useDetailCascade } from '../../composables/usePriceSetRefData'
|
||||
import { usePriceSetRefData } from '../../composables/usePriceSetRefData'
|
||||
import { getPriceSet } from '@/api/price/set'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -46,13 +47,13 @@ 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 products = ref([])
|
||||
|
||||
const title = computed(() => '编辑产品价格 · ' + (version.versionNo || ''))
|
||||
|
||||
@@ -76,8 +77,8 @@ function load() {
|
||||
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))
|
||||
details.value = []
|
||||
products.value = (current.products || []).map(p => ({ ...p }))
|
||||
loaded.value = true
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
|
||||
@@ -9,16 +9,40 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="showBuildActions" class="build-choice">
|
||||
<p class="build-choice__tip">暂无套餐销售价,请选择一种建价方式</p>
|
||||
<div class="build-choice__cards">
|
||||
<button
|
||||
type="button"
|
||||
class="build-card build-card--pull"
|
||||
:disabled="!priceSetId"
|
||||
@click="handlePullLibrary"
|
||||
>
|
||||
<div class="build-card__title">拉取产品列表</div>
|
||||
<div class="build-card__desc">按产品库基础价生成本版本套餐销售价</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="build-card build-card--import"
|
||||
:disabled="!priceSetId || !isFixedType(setType)"
|
||||
@click="openImport"
|
||||
>
|
||||
<div class="build-card__title">导入 Excel 建价</div>
|
||||
<div class="build-card__desc">上传威狮B2C小包价表,直接生成套餐销售价</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="filters">
|
||||
<el-input v-model="query.keyword" clearable :placeholder="phKeyword" style="width: 180px" />
|
||||
<el-select v-model="query.taxMode" clearable :placeholder="phTax" style="width: 140px">
|
||||
<el-option v-for="d in bms_tax_mode" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
<el-button size="small" @click="noop">查询</el-button>
|
||||
<el-button size="small" type="primary" plain :disabled="!priceSetId || !isFixedType(setType)" @click="openImport">导入修改价格</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredProducts" border empty-text="暂无产品价格明细">
|
||||
<el-table :data="filteredProducts" border>
|
||||
<el-table-column :label="colCode" min-width="80">
|
||||
<template #default="{ row }"><b>{{ row.productCode }}</b></template>
|
||||
</el-table-column>
|
||||
@@ -39,11 +63,12 @@
|
||||
<el-table-column :label="colAction" width="220" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openPrice(row)">编辑价格</el-button>
|
||||
<el-button link type="primary" @click="openSurcharge(row)">附加费</el-button>
|
||||
<el-button link type="primary" @click="openSurchargeForProduct(row)">附加费</el-button>
|
||||
<el-button link type="danger" @click="removeProduct(row.productCode)">移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<surcharge-picker-dialog
|
||||
v-model="scVisible"
|
||||
@@ -78,7 +103,7 @@
|
||||
|
||||
<excel-import-dialog
|
||||
ref="importRef"
|
||||
title="导入修改套餐价格"
|
||||
title="导入 Excel 建价"
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
:import-desc="importDesc"
|
||||
@@ -88,7 +113,7 @@
|
||||
|
||||
<el-dialog
|
||||
v-model="importProgressVisible"
|
||||
title="正在导入价格"
|
||||
:title="progressDialogTitle"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
@@ -117,10 +142,11 @@ import {
|
||||
surchargeAppliesToRow,
|
||||
toDetailPayload
|
||||
} from '../../utils/priceSet'
|
||||
import { getPriceSet, importSetDetailPricesAsync, updateSetDetail, updateSetDetailSurchargeAsync, delSetDetail } from '@/api/price/set'
|
||||
import { getPriceSet, listSetDetailByProduct, importSetDetailPricesAsync, pullSetDetailsFromLibraryAsync, updateSetDetail, updateSetDetailSurchargeAsync, delSetDetail } from '@/api/price/set'
|
||||
|
||||
const props = defineProps({
|
||||
details: { type: Array, required: true },
|
||||
products: { type: Array, default: () => [] },
|
||||
priceSetId: { type: [Number, String], default: undefined },
|
||||
psvId: { type: [Number, String], default: undefined },
|
||||
setType: { type: String, default: 'FIXED' },
|
||||
@@ -170,33 +196,52 @@ const importProgressVisible = ref(false)
|
||||
const importProgressPercent = ref(0)
|
||||
const importProgressMessage = ref('')
|
||||
const importProgressStatus = ref(undefined)
|
||||
const progressDialogTitle = ref('正在导入价格')
|
||||
const importDesc = [
|
||||
'适用威狮B2C小包价表(含「产品目录」)。',
|
||||
'按产品+国家+分区(有则匹配)+重量段匹配已有套餐明细,仅更新运费/挂号费。',
|
||||
'仅固定价套餐可导入;未匹配行不会新增明细。'
|
||||
'按产品+国家+分区(有则匹配)+重量段:命中则更新运费/挂号费,未命中则新增套餐明细。',
|
||||
'仅固定价套餐可导入;可与「拉取产品列表」二选一或叠加使用。'
|
||||
]
|
||||
|
||||
const typeHint = computed(() => '· 当前:' + setTypeLabel(props.setType))
|
||||
|
||||
const productGroups = computed(() => {
|
||||
const map = new Map()
|
||||
for (const d of props.details) {
|
||||
if (!d.productCode) continue
|
||||
if (!map.has(d.productCode)) {
|
||||
const meta = props.refData.productMetaOf(d.productCode) || {}
|
||||
map.set(d.productCode, {
|
||||
productCode: d.productCode,
|
||||
productName: d.productName || meta.name || d.productCode,
|
||||
serviceLevel: meta.serviceLevel,
|
||||
cargoAttr: meta.cargoAttr,
|
||||
destRegion: meta.destRegion,
|
||||
taxMode: meta.taxMode,
|
||||
rows: []
|
||||
})
|
||||
/** Show pull/import when this version has no set-product rows yet. */
|
||||
const showBuildActions = computed(() => !(props.products || []).some(p => p && p.productCode))
|
||||
|
||||
/** productCode -> detail rows (loaded on demand) */
|
||||
const detailByProduct = reactive({})
|
||||
|
||||
function clearDetailCache() {
|
||||
Object.keys(detailByProduct).forEach(k => delete detailByProduct[k])
|
||||
}
|
||||
|
||||
async function loadProductDetails(productCode, force) {
|
||||
if (!productCode) return []
|
||||
if (!force && detailByProduct[productCode]) return detailByProduct[productCode]
|
||||
if (!props.psvId) return []
|
||||
const res = await listSetDetailByProduct(props.psvId, productCode)
|
||||
const rows = (res.data || []).map(d => hydrateRow(d, props.setType))
|
||||
detailByProduct[productCode] = rows
|
||||
// keep parent details array in sync for any legacy callers
|
||||
if (Array.isArray(props.details)) {
|
||||
for (let i = props.details.length - 1; i >= 0; i--) {
|
||||
if (props.details[i] && props.details[i].productCode === productCode) props.details.splice(i, 1)
|
||||
}
|
||||
map.get(d.productCode).rows.push(d)
|
||||
props.details.push(...rows)
|
||||
}
|
||||
return [...map.values()]
|
||||
return rows
|
||||
}
|
||||
|
||||
const productGroups = computed(() => {
|
||||
return (props.products || []).filter(p => p && p.productCode).map(p => ({
|
||||
productCode: p.productCode,
|
||||
productName: p.productName || p.productCode,
|
||||
serviceLevel: p.serviceLevel,
|
||||
cargoAttr: p.cargoAttr,
|
||||
destRegion: p.destRegion,
|
||||
taxMode: p.taxMode,
|
||||
rows: detailByProduct[p.productCode] || []
|
||||
}))
|
||||
})
|
||||
|
||||
const filteredProducts = computed(() => {
|
||||
@@ -231,6 +276,7 @@ async function runImportAsync(uploadFile) {
|
||||
if (!raw) {
|
||||
throw new Error('请先选择文件')
|
||||
}
|
||||
progressDialogTitle.value = '正在导入价格'
|
||||
importProgressVisible.value = true
|
||||
importProgressPercent.value = 0
|
||||
importProgressMessage.value = '上传并解析中…'
|
||||
@@ -239,24 +285,7 @@ async function runImportAsync(uploadFile) {
|
||||
const { task } = await importSetDetailPricesAsync(
|
||||
raw,
|
||||
{ priceSetId: props.priceSetId, psvId: props.psvId },
|
||||
(t) => {
|
||||
importProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||
if (t.status === 'PENDING') importProgressMessage.value = '排队中…'
|
||||
else if (t.status === 'RUNNING') {
|
||||
const m = t.message || ''
|
||||
if (m === 'parsing excel') importProgressMessage.value = '正在解析价表…'
|
||||
else if (m === 'matching details') importProgressMessage.value = '正在匹配并更新价格…'
|
||||
else if (m === 'finishing') importProgressMessage.value = '收尾中…'
|
||||
else if (m === 'queued') importProgressMessage.value = '排队中…'
|
||||
else importProgressMessage.value = m || '正在导入…'
|
||||
} else if (t.status === 'SUCCESS') {
|
||||
importProgressMessage.value = t.message || '导入完成'
|
||||
importProgressStatus.value = 'success'
|
||||
} else if (t.status === 'FAIL') {
|
||||
importProgressMessage.value = t.error || t.message || '导入失败'
|
||||
importProgressStatus.value = 'exception'
|
||||
}
|
||||
}
|
||||
applyBuildProgress
|
||||
)
|
||||
importProgressPercent.value = 100
|
||||
importProgressStatus.value = 'success'
|
||||
@@ -272,79 +301,113 @@ async function runImportAsync(uploadFile) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyBuildProgress(t) {
|
||||
importProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||
if (t.status === 'PENDING') importProgressMessage.value = '排队中…'
|
||||
else if (t.status === 'RUNNING') {
|
||||
const m = t.message || ''
|
||||
if (m === 'parsing excel') importProgressMessage.value = '正在解析价表…'
|
||||
else if (m === 'matching details') importProgressMessage.value = '正在匹配并更新价格…'
|
||||
else if (m === 'pulling library') importProgressMessage.value = '正在拉取产品库…'
|
||||
else if (m === 'finishing') importProgressMessage.value = '收尾中…'
|
||||
else if (m === 'queued') importProgressMessage.value = '排队中…'
|
||||
else if (m.indexOf('importing ') === 0) importProgressMessage.value = '正在导入产品 ' + m.slice('importing '.length)
|
||||
else if (m.indexOf('saving details ') === 0) importProgressMessage.value = '正在保存明细 ' + m.slice('saving details '.length)
|
||||
else if (m === 'details exist, skip pull') importProgressMessage.value = '明细已存在,已跳过拉取'
|
||||
else if (m === 'saving surcharges') importProgressMessage.value = '正在保存附加费…'
|
||||
else importProgressMessage.value = m || '处理中…'
|
||||
} else if (t.status === 'SUCCESS') {
|
||||
importProgressMessage.value = t.message || '完成'
|
||||
importProgressStatus.value = 'success'
|
||||
} else if (t.status === 'FAIL') {
|
||||
importProgressMessage.value = t.error || t.message || '失败'
|
||||
importProgressStatus.value = 'exception'
|
||||
}
|
||||
}
|
||||
|
||||
async function onImportSuccess() {
|
||||
await reloadDetailsFromServer()
|
||||
}
|
||||
|
||||
async function reloadDetailsFromServer() {
|
||||
if (!props.priceSetId) return
|
||||
try {
|
||||
clearDetailCache()
|
||||
const res = await getPriceSet(props.priceSetId)
|
||||
const data = res.data || {}
|
||||
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] || {}
|
||||
const src = (current.details && current.details.length) ? current.details : (data.details || [])
|
||||
props.details.splice(0, props.details.length, ...src.map(d => hydrateRow(d, props.setType)))
|
||||
if (Array.isArray(props.details)) {
|
||||
props.details.splice(0, props.details.length)
|
||||
}
|
||||
if (Array.isArray(props.products)) {
|
||||
props.products.splice(0, props.products.length, ...(current.products || []).map(p => ({ ...p })))
|
||||
}
|
||||
if (priceVisible.value && editCode.value) {
|
||||
const group = productGroups.value.find(p => p.productCode === editCode.value)
|
||||
editRows.value = group ? group.rows : []
|
||||
editGroup.value = group || null
|
||||
await stampPriceFloors(editRows.value, editCode.value)
|
||||
const rows = await loadProductDetails(editCode.value, true)
|
||||
editRows.value = rows
|
||||
editGroup.value = productGroups.value.find(p => p.productCode === editCode.value) || null
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/** enter step 3: pull all library products + country prices (skip already added) */
|
||||
async function handlePullLibrary() {
|
||||
if (!props.priceSetId) {
|
||||
proxy.$modal.msgWarning('缺少套餐ID')
|
||||
return
|
||||
}
|
||||
const hasProducts = (props.products || []).some(p => p && p.productCode)
|
||||
let force = false
|
||||
if (hasProducts) {
|
||||
try {
|
||||
await proxy.$modal.confirm('当前版本已有明细,拉取将清空并按产品库重建,是否继续?')
|
||||
force = true
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
}
|
||||
await pullAllFromLibrary({ force, silent: false })
|
||||
}
|
||||
|
||||
/** Backend pull product/product_country into prc_price_set_detail (async). */
|
||||
async function pullAllFromLibrary(opts = {}) {
|
||||
const silent = !!opts.silent
|
||||
const replace = !!opts.replace
|
||||
if (!(props.refData.productOptions.value || []).length) {
|
||||
await props.refData.loadProducts()
|
||||
}
|
||||
const products = props.refData.productOptions.value || []
|
||||
if (!products.length) {
|
||||
if (!silent) proxy.$modal.msgWarning('产品库无可用产品')
|
||||
const force = !!opts.force || !!opts.replace
|
||||
if (!props.priceSetId) {
|
||||
if (!silent) proxy.$modal.msgWarning('缺少套餐ID')
|
||||
return { added: 0 }
|
||||
}
|
||||
if (replace) {
|
||||
props.details.splice(0, props.details.length)
|
||||
}
|
||||
const used = new Set((props.details || []).map(d => d.productCode).filter(Boolean))
|
||||
const toAdd = products.filter(p => !used.has(p.value))
|
||||
if (!toAdd.length) return { added: 0 }
|
||||
|
||||
progressDialogTitle.value = '正在拉取产品列表'
|
||||
importProgressVisible.value = true
|
||||
importProgressPercent.value = 0
|
||||
importProgressMessage.value = '排队中…'
|
||||
importProgressStatus.value = undefined
|
||||
pulling.value = true
|
||||
let added = 0
|
||||
try {
|
||||
// parallel fetch countries/surcharges, then push rows
|
||||
await Promise.all(toAdd.map(async (p) => {
|
||||
const code = p.value
|
||||
const name = p.name || props.refData.productNameOf(code)
|
||||
await props.refData.ensureSurcharges(code)
|
||||
if (isDiscountType(props.setType)) {
|
||||
const row = createEmptyDetailRow()
|
||||
row.productCode = code
|
||||
row.productName = name
|
||||
row.surcharges = props.refData.surchargesOf(row)
|
||||
props.details.push(row)
|
||||
added++
|
||||
return
|
||||
}
|
||||
const open = await props.refData.ensureCountries(code, props.effectiveTime)
|
||||
if (!open.length) return
|
||||
open.forEach(item => {
|
||||
const row = countryToDetail(item, code, name)
|
||||
row.surcharges = props.refData.surchargesOf(row)
|
||||
props.details.push(row)
|
||||
})
|
||||
added++
|
||||
}))
|
||||
const { task } = await pullSetDetailsFromLibraryAsync(
|
||||
{ priceSetId: props.priceSetId, psvId: props.psvId, force },
|
||||
applyBuildProgress
|
||||
)
|
||||
importProgressPercent.value = 100
|
||||
importProgressStatus.value = 'success'
|
||||
await reloadDetailsFromServer()
|
||||
if (!silent) {
|
||||
if (added > 0) proxy.$modal.msgSuccess('已从产品库全量拉取 ' + added + ' 个产品')
|
||||
else proxy.$modal.msgWarning('未拉取到可展示的产品价格明细,请检查产品库国家配置')
|
||||
proxy.$modal.msgSuccess((task && task.message) ? task.message : '拉取完成')
|
||||
}
|
||||
return { added: (props.products || []).length }
|
||||
} catch (e) {
|
||||
importProgressStatus.value = 'exception'
|
||||
const tip = (e && e.task && e.task.error) || (e && e.message) || '拉取失败'
|
||||
importProgressMessage.value = tip
|
||||
if (!silent) proxy.$modal.msgError(tip)
|
||||
await new Promise(resolve => setTimeout(resolve, 600))
|
||||
return { added: 0 }
|
||||
} finally {
|
||||
pulling.value = false
|
||||
importProgressVisible.value = false
|
||||
}
|
||||
return { added }
|
||||
}
|
||||
|
||||
function countryToDetail(item, productCode, productName) {
|
||||
@@ -362,10 +425,6 @@ function countryToDetail(item, productCode, productName) {
|
||||
row.productCountryId = item.detailId
|
||||
const baseFv = item.freightPrice != null ? Number(item.freightPrice) : undefined
|
||||
const baseReg = item.registrationFee != null ? Number(item.registrationFee) : undefined
|
||||
row._baseFreight = baseFv
|
||||
row._baseReg = baseReg
|
||||
row._floorFreight = baseFv
|
||||
row._floorReg = baseReg
|
||||
row.fvValue = baseFv
|
||||
row.fvRegistrationFee = baseReg
|
||||
row.effectiveDate = item.effectiveDate
|
||||
@@ -374,22 +433,39 @@ function countryToDetail(item, productCode, productName) {
|
||||
return hydrateRow(row, props.setType)
|
||||
}
|
||||
|
||||
function openPrice(row) {
|
||||
async function openPrice(row) {
|
||||
if (isDiscountType(props.setType)) {
|
||||
discCode.value = row.productCode
|
||||
discTitle.value = row.productCode + ' · ' + (row.productName || '') + ' · 产品折扣'
|
||||
const hit = (props.details || []).find(d => d.productCode === row.productCode)
|
||||
discValue.value = hit && hit.discountValue != null ? Number(hit.discountValue) : undefined
|
||||
try {
|
||||
const rows = await loadProductDetails(row.productCode)
|
||||
const hit = rows[0]
|
||||
discValue.value = hit && hit.discountValue != null ? Number(hit.discountValue) : undefined
|
||||
} catch (e) {
|
||||
discValue.value = undefined
|
||||
}
|
||||
discVisible.value = true
|
||||
return
|
||||
}
|
||||
editCode.value = row.productCode
|
||||
editName.value = row.productName || ''
|
||||
editRows.value = row.rows || []
|
||||
editGroup.value = row
|
||||
Promise.resolve(stampPriceFloors(editRows.value, row.productCode)).finally(() => {
|
||||
try {
|
||||
editRows.value = await loadProductDetails(row.productCode)
|
||||
priceVisible.value = true
|
||||
})
|
||||
} catch (e) {
|
||||
proxy.$modal.msgError((e && e.message) ? e.message : '加载产品价格失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** list-row surcharge: load that product details first */
|
||||
async function openSurchargeForProduct(row) {
|
||||
try {
|
||||
const rows = await loadProductDetails(row.productCode)
|
||||
await openSurcharge({ ...row, rows })
|
||||
} catch (e) {
|
||||
proxy.$modal.msgError((e && e.message) ? e.message : '加载产品明细失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 抽屉里点附加费:带行 = 行级(只改该行,国家+重量段),不带 = 产品级(覆盖全部明细行) */
|
||||
@@ -437,47 +513,6 @@ async function onRowPriceChange(row) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyImportedFloor(row, hit) {
|
||||
if (!row || !hit) return
|
||||
if (hit.freightPrice != null) {
|
||||
row._floorFreight = Number(hit.freightPrice)
|
||||
row._baseFreight = Number(hit.freightPrice)
|
||||
}
|
||||
if (hit.registrationFee != null) {
|
||||
row._floorReg = Number(hit.registrationFee)
|
||||
row._baseReg = Number(hit.registrationFee)
|
||||
}
|
||||
}
|
||||
|
||||
function findImportedCountry(rows, row) {
|
||||
if (!rows || !rows.length || !row) return null
|
||||
if (row.productCountryId != null) {
|
||||
const byId = rows.find(item => Number(item.detailId) === Number(row.productCountryId))
|
||||
if (byId) return byId
|
||||
}
|
||||
return rows.find(item => {
|
||||
const sameCountry = (row.countryCode && item.countryCode
|
||||
&& String(row.countryCode).toUpperCase() === String(item.countryCode).toUpperCase())
|
||||
|| (!row.countryCode && row.countryName && item.countryName && row.countryName === item.countryName)
|
||||
if (!sameCountry) return false
|
||||
const rz = String(row.zoneCode || '').toUpperCase()
|
||||
const iz = String(item.zoneCode || '').toUpperCase()
|
||||
if (rz || iz) {
|
||||
if (rz !== iz) return false
|
||||
}
|
||||
if (row.weightMin != null && item.weightMin != null && Number(row.weightMin) !== Number(item.weightMin)) return false
|
||||
if (row.weightMax != null && item.weightMax != null && Number(row.weightMax) !== Number(item.weightMax)) return false
|
||||
return row.weightMin != null || row.weightMax != null
|
||||
}) || null
|
||||
}
|
||||
|
||||
function stampPriceFloors(rows, productCode) {
|
||||
if (!props.refData || !productCode || !rows || !rows.length) return Promise.resolve()
|
||||
return props.refData.ensureCountries(productCode, props.effectiveTime).then(list => {
|
||||
rows.forEach(row => applyImportedFloor(row, findImportedCountry(list, row)))
|
||||
})
|
||||
}
|
||||
|
||||
async function saveDiscount() {
|
||||
const hit = (props.details || []).find(d => d.productCode === discCode.value)
|
||||
if (hit) {
|
||||
@@ -607,16 +642,25 @@ function onSurchargeConfirm(picked) {
|
||||
}
|
||||
|
||||
async function removeProduct(code) {
|
||||
const ids = props.details.filter(d => d.productCode === code && d.psdId).map(d => d.psdId)
|
||||
if (ids.length) {
|
||||
try {
|
||||
try {
|
||||
const rows = await loadProductDetails(code)
|
||||
const ids = rows.filter(d => d && d.psdId).map(d => d.psdId)
|
||||
if (ids.length) {
|
||||
await delSetDetail(ids.join(','))
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
delete detailByProduct[code]
|
||||
if (Array.isArray(props.details)) {
|
||||
for (let i = props.details.length - 1; i >= 0; i--) {
|
||||
if (props.details[i] && props.details[i].productCode === code) props.details.splice(i, 1)
|
||||
}
|
||||
}
|
||||
for (let i = props.details.length - 1; i >= 0; i--) {
|
||||
if (props.details[i].productCode === code) props.details.splice(i, 1)
|
||||
if (Array.isArray(props.products)) {
|
||||
for (let i = props.products.length - 1; i >= 0; i--) {
|
||||
if (props.products[i] && props.products[i].productCode === code) props.products.splice(i, 1)
|
||||
}
|
||||
}
|
||||
proxy.$modal.msgSuccess('已移除产品明细')
|
||||
}
|
||||
@@ -647,6 +691,58 @@ defineExpose({ pullAllFromLibrary })
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.build-choice {
|
||||
padding: 48px 24px 56px;
|
||||
text-align: center;
|
||||
}
|
||||
.build-choice__tip {
|
||||
margin: 0 0 28px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
.build-choice__cards {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.build-card {
|
||||
width: 280px;
|
||||
min-height: 120px;
|
||||
padding: 24px 20px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.build-card:hover:not(:disabled) {
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.build-card:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.build-card--pull:hover:not(:disabled) {
|
||||
border-color: #67c23a;
|
||||
}
|
||||
.build-card--import:hover:not(:disabled) {
|
||||
border-color: #409eff;
|
||||
}
|
||||
.build-card__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.build-card--pull .build-card__title { color: #67c23a; }
|
||||
.build-card--import .build-card__title { color: #409eff; }
|
||||
.build-card__desc {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #909399;
|
||||
}
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
v-show="active === 1"
|
||||
ref="detailsRef"
|
||||
:details="form.details"
|
||||
:products="form.products"
|
||||
:price-set-id="form.priceSetId"
|
||||
:psv-id="form.version.psvId"
|
||||
:set-type="form.basic.setType"
|
||||
@@ -49,7 +50,7 @@
|
||||
<script setup name="PriceSetCreate">
|
||||
import StepBasicInfo from './components/create/StepBasicInfo.vue'
|
||||
import StepDetails from './components/create/StepDetails.vue'
|
||||
import { usePriceSetRefData, useDetailCascade } from './composables/usePriceSetRefData'
|
||||
import { usePriceSetRefData } from './composables/usePriceSetRefData'
|
||||
import {
|
||||
createEmptyBasicInfo,
|
||||
nextVersionNo,
|
||||
@@ -62,7 +63,6 @@ const { proxy } = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
|
||||
const refData = usePriceSetRefData()
|
||||
const cascade = useDetailCascade(refData)
|
||||
|
||||
const active = ref(0)
|
||||
const saving = ref(false)
|
||||
@@ -85,6 +85,7 @@ const form = reactive({
|
||||
basic: createEmptyBasicInfo(),
|
||||
version: { psvId: undefined, versionNo: 'V1', effectiveTime: undefined, expireTime: undefined },
|
||||
details: [],
|
||||
products: [],
|
||||
customers: []
|
||||
})
|
||||
|
||||
@@ -140,30 +141,24 @@ async function runSaveWithProgress(payload) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Step1 -> Step2: create package prices from library when needed */
|
||||
/** Step1 -> Step2: create empty draft version; pull/import on Step 2 */
|
||||
async function goStep2() {
|
||||
const validBasic = await basicRef.value.validate()
|
||||
if (!validBasic) return
|
||||
|
||||
const hasPersistedDetails = (form.details || []).some(d => d && d.psdId)
|
||||
if (form.priceSetId && form.version.psvId && hasPersistedDetails) {
|
||||
if (form.priceSetId && form.version.psvId) {
|
||||
active.value = 1
|
||||
return
|
||||
}
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
form.details.splice(0, form.details.length)
|
||||
const priceSetId = await runSaveWithProgress({
|
||||
...buildPayload(),
|
||||
details: [],
|
||||
pullFromLibrary: 'Y'
|
||||
})
|
||||
const priceSetId = await runSaveWithProgress(buildPayload())
|
||||
form.priceSetId = priceSetId
|
||||
const fresh = await getPriceSet(priceSetId)
|
||||
applySetData(fresh.data || {}, false)
|
||||
active.value = 1
|
||||
proxy.$modal.msgSuccess('已创建套餐价格,可在第 2 步编辑')
|
||||
proxy.$modal.msgSuccess('已创建空套餐版本,请在第 2 步拉取或导入价格')
|
||||
} catch (e) {
|
||||
// surfaced
|
||||
} finally {
|
||||
@@ -269,12 +264,8 @@ function applySetData(data, asCopy) {
|
||||
form.version.effectiveTime = current.effectiveTime
|
||||
form.version.expireTime = current.expireTime
|
||||
}
|
||||
const srcDetails = (current.details && current.details.length) ? current.details : (data.details || [])
|
||||
form.details = srcDetails.map(d => {
|
||||
const row = cascade.hydrateRow(d, form.basic.setType)
|
||||
if (asCopy) row.psdId = undefined
|
||||
return row
|
||||
})
|
||||
form.details = []
|
||||
form.products = asCopy ? [] : ((current.products || []).map(p => ({ ...p })))
|
||||
form.customers = (data.customers || []).map(c => ({ ...c }))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user