Compare commits
2 Commits
2632e5aa78
...
de5784cbac
| Author | SHA1 | Date | |
|---|---|---|---|
| de5784cbac | |||
| 16afa0d590 |
+303
@@ -0,0 +1,303 @@
|
||||
package com.bms.web.controller.price;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.bms.common.annotation.Log;
|
||||
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.exception.ServiceException;
|
||||
import com.bms.price.domain.PaySaveCommand;
|
||||
import com.bms.price.domain.PrcCustomerPriceSet;
|
||||
import com.bms.price.domain.PrcCustomerPriceSetBatch;
|
||||
import com.bms.price.domain.PrcPriceSet;
|
||||
import com.bms.price.service.IPayProductService;
|
||||
import com.bms.price.service.IPrcPriceSetService;
|
||||
|
||||
/**
|
||||
* 打单套餐:套餐壳 + 应收价卡(pay_*)。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/price/label")
|
||||
public class LabelPackageController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private IPrcPriceSetService priceSetService;
|
||||
|
||||
@Autowired
|
||||
private IPayProductService payProductService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(PrcPriceSet set)
|
||||
{
|
||||
set.setSetCategory(PrcPriceSet.CATEGORY_LABEL);
|
||||
startPage();
|
||||
List<PrcPriceSet> list = priceSetService.selectPrcPriceSetList(set);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:query')")
|
||||
@GetMapping("/{priceSetId:[0-9]+}")
|
||||
public AjaxResult getInfo(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
return success(priceSetService.selectPrcPriceSetById(priceSetId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:edit')")
|
||||
@Log(title = "打单套餐基本信息", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/basic")
|
||||
public AjaxResult editBasic(@RequestBody PrcPriceSet set)
|
||||
{
|
||||
priceSetService.assertSetCategory(set.getPriceSetId(), PrcPriceSet.CATEGORY_LABEL);
|
||||
set.setUpdateBy(getUsername());
|
||||
return toAjax(priceSetService.updateBasicInfo(set));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:audit')")
|
||||
@Log(title = "打单套餐停用", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{priceSetId}/disable")
|
||||
public AjaxResult disable(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
return toAjax(priceSetService.disable(priceSetId, getUsername()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:audit')")
|
||||
@Log(title = "打单套餐启用", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{priceSetId}/enable")
|
||||
public AjaxResult enable(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
return toAjax(priceSetService.enable(priceSetId, getUsername()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:remove')")
|
||||
@Log(title = "打单套餐删除", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{priceSetIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] priceSetIds)
|
||||
{
|
||||
if (priceSetIds != null)
|
||||
{
|
||||
for (Long id : priceSetIds)
|
||||
{
|
||||
priceSetService.assertSetCategory(id, PrcPriceSet.CATEGORY_LABEL);
|
||||
}
|
||||
}
|
||||
return toAjax(priceSetService.deletePrcPriceSetByIds(priceSetIds));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:import')")
|
||||
@PostMapping("/preview")
|
||||
public AjaxResult preview(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "dictValue", required = false) String dictValue,
|
||||
@RequestParam(value = "progressId", required = false) String progressId,
|
||||
@RequestParam(value = "priceSetId", required = false) Long priceSetId) throws Exception
|
||||
{
|
||||
if (file == null || file.isEmpty())
|
||||
{
|
||||
throw new ServiceException("请上传价卡文件");
|
||||
}
|
||||
String name = file.getOriginalFilename();
|
||||
if (name == null || !name.toLowerCase(Locale.ROOT).endsWith(".xlsx"))
|
||||
{
|
||||
throw new ServiceException("请上传xlsx价卡");
|
||||
}
|
||||
if (priceSetId != null)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
}
|
||||
// 打单独立预览:已存在按本套餐判断,不与应付公共模板互挡
|
||||
return success(payProductService.previewLabel(file.getBytes(), name, dictValue, progressId, priceSetId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:import')")
|
||||
@GetMapping("/preview/progress")
|
||||
public AjaxResult parseProgress(@RequestParam("progressId") String progressId)
|
||||
{
|
||||
return success(payProductService.parseProgress(progressId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:import')")
|
||||
@Log(title = "打单套餐导入", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/importSave")
|
||||
public AjaxResult importSave(@RequestBody PaySaveCommand command)
|
||||
{
|
||||
if (command == null)
|
||||
{
|
||||
throw new ServiceException("保存参数不能为空");
|
||||
}
|
||||
String message = payProductService.saveLabel(command);
|
||||
Map<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("message", message);
|
||||
payload.put("priceSetId", command.getPriceSetId());
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:list')")
|
||||
@GetMapping("/{priceSetId}/versions")
|
||||
public AjaxResult versions(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
return success(payProductService.selectLabelVersions(priceSetId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:edit')")
|
||||
@Log(title = "打单套餐启动版本", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{priceSetId}/versions/{versionNo}/activate")
|
||||
public AjaxResult activateVersion(@PathVariable Long priceSetId, @PathVariable Integer versionNo,
|
||||
@RequestBody Map<String, Object> body)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
Date effectiveTime = parseEffectiveTime(body == null ? null : body.get("effectiveTime"));
|
||||
String message = payProductService.activateLabelVersion(priceSetId, versionNo, effectiveTime);
|
||||
Map<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("message", message);
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:list')")
|
||||
@GetMapping("/{priceSetId}/products")
|
||||
public AjaxResult products(@PathVariable Long priceSetId,
|
||||
@RequestParam(value = "versionNo", required = false) Integer versionNo)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
if (versionNo != null)
|
||||
{
|
||||
return success(payProductService.selectLabelProductsByVersion(priceSetId, versionNo));
|
||||
}
|
||||
return success(payProductService.selectLabelProducts(priceSetId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:query')")
|
||||
@GetMapping("/product/{productId}")
|
||||
public AjaxResult productDetail(@PathVariable Long productId,
|
||||
@RequestParam(value = "termId", required = false) Long termId)
|
||||
{
|
||||
if (termId != null)
|
||||
{
|
||||
return success(payProductService.selectPayProductDetailByTerm(termId));
|
||||
}
|
||||
return success(payProductService.selectPayProductDetail(productId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:remove')")
|
||||
@Log(title = "打单套餐移除产品", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{priceSetId}/products/{productIds}")
|
||||
public AjaxResult removeProducts(@PathVariable Long priceSetId, @PathVariable Long[] productIds)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
return toAjax(payProductService.removeLabelProducts(priceSetId, productIds));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:list')")
|
||||
@GetMapping("/customer/list")
|
||||
public TableDataInfo customerList(PrcCustomerPriceSet customer)
|
||||
{
|
||||
if (customer != null && customer.getPriceSetId() != null)
|
||||
{
|
||||
priceSetService.assertSetCategory(customer.getPriceSetId(), PrcPriceSet.CATEGORY_LABEL);
|
||||
}
|
||||
startPage();
|
||||
List<PrcCustomerPriceSet> list = priceSetService.selectCustomerList(customer);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:edit')")
|
||||
@Log(title = "打单套餐客户绑定", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/customer")
|
||||
public AjaxResult addCustomer(@RequestBody PrcCustomerPriceSet customer)
|
||||
{
|
||||
if (customer == null || customer.getPriceSetId() == null)
|
||||
{
|
||||
throw new ServiceException("请选择打单套餐");
|
||||
}
|
||||
priceSetService.assertSetCategory(customer.getPriceSetId(), PrcPriceSet.CATEGORY_LABEL);
|
||||
customer.setCreateBy(getUsername());
|
||||
return toAjax(priceSetService.insertCustomer(customer));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:edit')")
|
||||
@Log(title = "打单套餐客户批量绑定", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/customer/batch")
|
||||
public AjaxResult batchAddCustomer(@RequestBody PrcCustomerPriceSetBatch batch)
|
||||
{
|
||||
if (batch == null || batch.getPriceSetId() == null)
|
||||
{
|
||||
throw new ServiceException("请选择打单套餐");
|
||||
}
|
||||
priceSetService.assertSetCategory(batch.getPriceSetId(), PrcPriceSet.CATEGORY_LABEL);
|
||||
batch.setCreateBy(getUsername());
|
||||
batch.setUpdateBy(getUsername());
|
||||
return success(priceSetService.batchInsertCustomers(batch));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:edit')")
|
||||
@Log(title = "打单套餐客户绑定", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/customer")
|
||||
public AjaxResult editCustomer(@RequestBody PrcCustomerPriceSet customer)
|
||||
{
|
||||
if (customer == null)
|
||||
{
|
||||
throw new ServiceException("客户绑定参数不能为空");
|
||||
}
|
||||
if (customer.getPriceSetId() != null)
|
||||
{
|
||||
priceSetService.assertSetCategory(customer.getPriceSetId(), PrcPriceSet.CATEGORY_LABEL);
|
||||
}
|
||||
customer.setUpdateBy(getUsername());
|
||||
return toAjax(priceSetService.updateCustomer(customer));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:edit')")
|
||||
@Log(title = "打单套餐客户绑定", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/customer/{customerPricesetIds}")
|
||||
public AjaxResult removeCustomer(@PathVariable Long[] customerPricesetIds)
|
||||
{
|
||||
return toAjax(priceSetService.deleteCustomerByIds(customerPricesetIds));
|
||||
}
|
||||
|
||||
private Date parseEffectiveTime(Object raw)
|
||||
{
|
||||
if (raw == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (raw instanceof Date)
|
||||
{
|
||||
return (Date) raw;
|
||||
}
|
||||
String text = String.valueOf(raw).trim();
|
||||
if (text.isEmpty() || "null".equalsIgnoreCase(text))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// 兼容 yyyy-MM-dd / yyyy-MM-dd HH:mm:ss
|
||||
if (text.length() >= 19)
|
||||
{
|
||||
return com.bms.common.utils.DateUtils.parseDate(text);
|
||||
}
|
||||
if (text.length() >= 10)
|
||||
{
|
||||
return com.bms.common.utils.DateUtils.parseDate(text.substring(0, 10));
|
||||
}
|
||||
return com.bms.common.utils.DateUtils.parseDate(text);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -135,7 +135,7 @@ public class PayProductController extends BaseController
|
||||
payProductService.unbindChannel(channelId, payType);
|
||||
return success();
|
||||
}
|
||||
payProductService.bindChannel(channelId, link.getProductId(), link.getPriceSetId(), payType);
|
||||
payProductService.bindChannel(channelId, link.getProductId(), payType);
|
||||
return success(payProductService.selectChannelPrice(channelId, payType));
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ public class ChannelProductMap
|
||||
|
||||
private Long productId;
|
||||
|
||||
/** 应收套餐 prc_price_set.price_set_id */
|
||||
private Long priceSetId;
|
||||
|
||||
/** AR 应收,AP 应付 */
|
||||
private String payType;
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 打单套餐价卡版本(按 pay_product_term.version_no 聚合,不走价格套餐 prc_price_set_version)。
|
||||
*/
|
||||
@Data
|
||||
public class LabelPackageVersion
|
||||
{
|
||||
private Long priceSetId;
|
||||
|
||||
private Integer versionNo;
|
||||
|
||||
/** 展示用,如 V1 */
|
||||
private String versionLabel;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date expireTime;
|
||||
|
||||
/** 与 pay_product_term.status 一致:0待生效 1生效 2已失效 */
|
||||
private String status;
|
||||
|
||||
private Integer productCount;
|
||||
|
||||
/** status=1 生效条数 */
|
||||
private Integer activeCount;
|
||||
|
||||
/** status=0 待生效条数 */
|
||||
private Integer pendingCount;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -12,6 +12,9 @@ public class PayExtraLine
|
||||
|
||||
private Long productId;
|
||||
|
||||
/** 价卡周期,对应 pay_product_term.term_id;与 productId 组成明细维度 */
|
||||
private Long termId;
|
||||
|
||||
/** 邮编 或 配置 */
|
||||
private String lineType;
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ public class PayProductTerm extends BaseEntity
|
||||
|
||||
private Long productId;
|
||||
|
||||
/** 打单套餐ID;应付公共模板为空 */
|
||||
private Long priceSetId;
|
||||
|
||||
private Integer versionNo;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
@@ -30,7 +33,7 @@ public class PayProductTerm extends BaseEntity
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveEnd;
|
||||
|
||||
/** 0 生效,1 失效 */
|
||||
/** 0待生效 1生效 2已失效 */
|
||||
private String status;
|
||||
|
||||
private String productName;
|
||||
|
||||
@@ -13,5 +13,17 @@ public class PaySaveCommand
|
||||
/** 为 true 时,同文件同表同名的旧产品先删后插 */
|
||||
private boolean overwrite;
|
||||
|
||||
/** 打单套餐ID;有值时走 LABEL 导入(稳定 product_id + term.price_set_id) */
|
||||
private Long priceSetId;
|
||||
|
||||
/** 打单套餐名称;priceSetId 为空时新建套餐壳 */
|
||||
private String priceSetName;
|
||||
|
||||
/** 打单套餐备注 */
|
||||
private String priceSetNote;
|
||||
|
||||
/** 编辑待生效版本时传入,覆盖该 version_no,不新建版本号 */
|
||||
private Integer versionNo;
|
||||
|
||||
private List<PayProduct> products = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ public class PaySurcharge
|
||||
|
||||
private Long productId;
|
||||
|
||||
/** 所属价卡版本,跟产品版本一起变 */
|
||||
/** 价卡周期,对应 pay_product_term.term_id;与 productId 组成明细维度 */
|
||||
private Long termId;
|
||||
|
||||
/** 所属价卡版本,跟产品版本一起变(展示兼容) */
|
||||
private Integer versionNo;
|
||||
|
||||
private String sectionName;
|
||||
|
||||
@@ -13,7 +13,10 @@ public class PayWeightBand
|
||||
|
||||
private Long productId;
|
||||
|
||||
/** 所属价卡版本,跟产品版本一起变 */
|
||||
/** 价卡周期,对应 pay_product_term.term_id;与 productId 组成明细维度 */
|
||||
private Long termId;
|
||||
|
||||
/** 所属价卡版本,跟产品版本一起变(展示兼容) */
|
||||
private Integer versionNo;
|
||||
|
||||
/** 报价区块,例如派送费、偏远附加费 */
|
||||
|
||||
@@ -19,6 +19,8 @@ public class PrcPriceSet extends BaseEntity
|
||||
public static final String CATEGORY_PACKAGE = "PACKAGE";
|
||||
/** Boss exclusive quote. */
|
||||
public static final String CATEGORY_EXCLUSIVE = "EXCLUSIVE";
|
||||
/** Label / 打单套餐(应收价卡套餐). */
|
||||
public static final String CATEGORY_LABEL = "LABEL";
|
||||
|
||||
private Long priceSetId;
|
||||
private String priceSetCode;
|
||||
@@ -26,7 +28,7 @@ public class PrcPriceSet extends BaseEntity
|
||||
private String priceSetNote;
|
||||
/** FIXED / DISCOUNT */
|
||||
private String setType;
|
||||
/** PACKAGE=ordinary set / EXCLUSIVE=boss exclusive quote */
|
||||
/** PACKAGE=ordinary set / EXCLUSIVE=boss exclusive quote / LABEL=打单套餐 */
|
||||
private String setCategory;
|
||||
private BigDecimal defaultDiscount;
|
||||
private String defaultType;
|
||||
|
||||
@@ -21,6 +21,11 @@ public class TmsPriceQuoteRequest implements Serializable
|
||||
@NotBlank(message = "productCode is required")
|
||||
private String productCode;
|
||||
|
||||
/**
|
||||
* 打单套餐(LABEL)试算/计费用渠道ID;计费也可从订单扩展 productChannelId 解析。
|
||||
*/
|
||||
private String channelId;
|
||||
|
||||
/** ISO2; countryName alternative */
|
||||
private String countryCode;
|
||||
/** country display name; countryCode alternative */
|
||||
|
||||
@@ -38,16 +38,33 @@ public interface PayProductMapper
|
||||
|
||||
List<PayWeightBand> selectWeightBands(@Param("productIds") List<Long> productIds);
|
||||
|
||||
List<PayWeightBand> selectWeightBandsByTerm(@Param("termIds") List<Long> termIds);
|
||||
|
||||
List<PaySurcharge> selectSurcharges(@Param("productIds") List<Long> productIds);
|
||||
|
||||
List<PaySurcharge> selectSurchargesByTerm(@Param("termIds") List<Long> termIds);
|
||||
|
||||
List<PayExtraLine> selectExtraLines(@Param("productIds") List<Long> productIds);
|
||||
|
||||
List<PayExtraLine> selectExtraLinesByTerm(@Param("termIds") List<Long> termIds);
|
||||
|
||||
int deleteWeightBands(@Param("productIds") List<Long> productIds);
|
||||
|
||||
int deleteSurcharges(@Param("productIds") List<Long> productIds);
|
||||
|
||||
int deleteExtraLines(@Param("productIds") List<Long> productIds);
|
||||
|
||||
int deleteWeightBandsByTerm(@Param("termIds") List<Long> termIds);
|
||||
|
||||
int deleteSurchargesByTerm(@Param("termIds") List<Long> termIds);
|
||||
|
||||
int deleteExtraLinesByTerm(@Param("termIds") List<Long> termIds);
|
||||
|
||||
Long selectTemplateIdByType(@Param("dictValue") String dictValue, @Param("productType") String productType);
|
||||
|
||||
/** 打单专用:命中仅属于打单套餐的 AR 产品,不命中应付公共模板 */
|
||||
Long selectLabelProductIdByDict(@Param("dictValue") String dictValue);
|
||||
|
||||
int deletePayProducts(@Param("productIds") List<Long> productIds);
|
||||
|
||||
List<Long> selectFamilyIds(@Param("rootId") Long rootId);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.bms.price.mapper;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.price.domain.LabelPackageVersion;
|
||||
import com.bms.price.domain.PayProductTerm;
|
||||
|
||||
/**
|
||||
@@ -16,6 +17,28 @@ public interface PayProductTermMapper
|
||||
|
||||
PayProductTerm selectByProductId(Long productId);
|
||||
|
||||
List<PayProductTerm> selectByPriceSetId(@Param("priceSetId") Long priceSetId);
|
||||
|
||||
List<PayProductTerm> selectByPriceSetIdAndVersion(@Param("priceSetId") Long priceSetId,
|
||||
@Param("versionNo") Integer versionNo);
|
||||
|
||||
/** 打单计费:套餐+产品+计费日命中 term(时间窗口优先) */
|
||||
Long selectTermIdBySetProductAt(@Param("priceSetId") Long priceSetId, @Param("productId") Long productId,
|
||||
@Param("bizDate") Date bizDate);
|
||||
|
||||
/** 打单套餐:按 version_no 聚合版本(含已失效) */
|
||||
List<LabelPackageVersion> selectLabelVersionGroups(@Param("priceSetId") Long priceSetId);
|
||||
|
||||
/** 打单套餐:批量改某一版的生效起止与状态 */
|
||||
int updateLabelVersionPeriod(@Param("priceSetId") Long priceSetId, @Param("versionNo") Integer versionNo,
|
||||
@Param("effectiveStart") Date effectiveStart, @Param("effectiveEnd") Date effectiveEnd,
|
||||
@Param("status") String status, @Param("updateBy") String updateBy);
|
||||
|
||||
Integer selectMaxVersionByProduct(@Param("productId") Long productId, @Param("priceSetId") Long priceSetId);
|
||||
|
||||
/** 打单套餐下最大 version_no */
|
||||
Integer selectMaxVersionByPriceSet(@Param("priceSetId") Long priceSetId);
|
||||
|
||||
int insertTerm(PayProductTerm term);
|
||||
|
||||
int updatePeriod(PayProductTerm term);
|
||||
@@ -25,7 +48,29 @@ public interface PayProductTermMapper
|
||||
/** 新版本生效时,把同一产品仍生效的旧版本关掉,结束日收到新版本开始的前一天 */
|
||||
int closeOpenTerms(@Param("dictValue") String dictValue, @Param("endDate") Date endDate, @Param("updateBy") String updateBy);
|
||||
|
||||
/** 打单套餐:按套餐+产品关闭仍生效的旧周期 */
|
||||
int closeOpenTermsBySetProduct(@Param("priceSetId") Long priceSetId, @Param("productId") Long productId,
|
||||
@Param("endDate") Date endDate, @Param("updateBy") String updateBy);
|
||||
|
||||
/** 打单:只关掉某产品本套餐待生效 term */
|
||||
int closePendingBySetProduct(@Param("priceSetId") Long priceSetId, @Param("productId") Long productId,
|
||||
@Param("endDate") Date endDate, @Param("updateBy") String updateBy);
|
||||
|
||||
/** 打单:启动某版本时,关掉本套餐其它版本的生效 term */
|
||||
int closeEffectExceptVersion(@Param("priceSetId") Long priceSetId, @Param("versionNo") Integer versionNo,
|
||||
@Param("endDate") Date endDate, @Param("updateBy") String updateBy);
|
||||
|
||||
/** 打单:预约未来生效时,只写旧版失效日,仍保持生效;并恢复被提前关掉但未到期的旧版 */
|
||||
int scheduleEndExceptVersion(@Param("priceSetId") Long priceSetId, @Param("versionNo") Integer versionNo,
|
||||
@Param("endDate") Date endDate, @Param("today") Date today, @Param("updateBy") String updateBy);
|
||||
|
||||
/** 打单:恢复「失效日未到却被标成已失效」的 term */
|
||||
int restorePrematureClosed(@Param("priceSetId") Long priceSetId, @Param("today") Date today,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
int deleteByProductIds(@Param("productIds") List<Long> productIds);
|
||||
|
||||
int deleteByTermIds(@Param("termIds") List<Long> termIds);
|
||||
|
||||
int updateProductEffectiveDate(@Param("productId") Long productId, @Param("effectiveDate") Date effectiveDate);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.bms.price.service;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.Date;
|
||||
import com.bms.price.domain.LabelPackageVersion;
|
||||
import com.bms.price.domain.PayParseProgress;
|
||||
import com.bms.price.domain.PayPreview;
|
||||
import com.bms.price.domain.PayPriceCompare;
|
||||
@@ -54,7 +55,34 @@ public interface IPayProductService
|
||||
PayProduct selectChannelPrice(String channelId, String payType);
|
||||
|
||||
/** 只绑定产品编号,不产生新版本;同时落 channel_code。payType 为空时按应付 AP */
|
||||
void bindChannel(String channelId, Long productId, Long priceSetId, String payType);
|
||||
void bindChannel(String channelId, Long productId, String payType);
|
||||
|
||||
/**
|
||||
* 打单套餐预览:解析复用应付解析器,但「已存在」只按本套餐 price_set_id 判断,
|
||||
* 不与应付公共模板互相挡导入。
|
||||
*/
|
||||
PayPreview previewLabel(byte[] bytes, String filename, String dictValue, String progressId, Long priceSetId);
|
||||
|
||||
/** 打单套餐导入:稳定 product_id,明细挂 term_id,term 写 price_set_id;跳过仅限本套餐已有生效版 */
|
||||
String saveLabel(PaySaveCommand command);
|
||||
|
||||
/** 打单套餐价卡版本列表(按 term.version_no 聚合) */
|
||||
List<LabelPackageVersion> selectLabelVersions(Long priceSetId);
|
||||
|
||||
/** 启动/修改打单套餐某一版生效时间,并视情况启用套餐 */
|
||||
String activateLabelVersion(Long priceSetId, Integer versionNo, Date effectiveTime);
|
||||
|
||||
/** 打单套餐内当前生效/待生效产品列表 */
|
||||
List<PayProduct> selectLabelProducts(Long priceSetId);
|
||||
|
||||
/** 打单套餐某一版本的产品列表 */
|
||||
List<PayProduct> selectLabelProductsByVersion(Long priceSetId, Integer versionNo);
|
||||
|
||||
/** 按 term 加载明细 */
|
||||
PayProduct selectPayProductDetailByTerm(Long termId);
|
||||
|
||||
/** 从打单套餐移除产品(失效/清理对应 term 明细) */
|
||||
int removeLabelProducts(Long priceSetId, Long[] productIds);
|
||||
|
||||
/** 取消渠道上指定收付类型的产品。payType 为空时按应付 AP */
|
||||
void unbindChannel(String channelId, String payType);
|
||||
@@ -62,6 +90,11 @@ public interface IPayProductService
|
||||
/** 按订单时间取这一产品当时有效的价格版本 */
|
||||
PayProduct selectPriceForDate(String channelId, Date bizDate);
|
||||
|
||||
/**
|
||||
* 打单套餐计费取价:渠道 AR 映射 → 稳定 product_id,再按套餐+计费日命中 term。
|
||||
*/
|
||||
PayProduct selectLabelPriceForDate(Long priceSetId, String channelId, Date bizDate);
|
||||
|
||||
/** 对照两个版本的重量段和附加费 */
|
||||
PayPriceCompare compare(Long leftId, Long rightId);
|
||||
|
||||
|
||||
+604
-26
@@ -25,6 +25,7 @@ import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.common.utils.file.OssStorage;
|
||||
import com.bms.common.utils.uuid.IdUtils;
|
||||
import com.bms.price.domain.ChannelProductMap;
|
||||
import com.bms.price.domain.LabelPackageVersion;
|
||||
import com.bms.price.domain.PayExtraLine;
|
||||
import com.bms.price.domain.PayParseProgress;
|
||||
import com.bms.price.domain.PayPreview;
|
||||
@@ -35,11 +36,13 @@ import com.bms.price.domain.PayProductTerm;
|
||||
import com.bms.price.domain.PaySaveCommand;
|
||||
import com.bms.price.domain.PaySurcharge;
|
||||
import com.bms.price.domain.PayWeightBand;
|
||||
import com.bms.price.domain.PrcPriceSet;
|
||||
import com.bms.masterdata.domain.ServiceChannel;
|
||||
import com.bms.masterdata.mapper.ServiceChannelMapper;
|
||||
import com.bms.price.mapper.ChannelProductMapMapper;
|
||||
import com.bms.price.mapper.PayProductMapper;
|
||||
import com.bms.price.mapper.PayProductTermMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetMapper;
|
||||
import com.bms.price.pay.PayParseProgressListener;
|
||||
import com.bms.price.pay.PayWorkbookParser;
|
||||
import com.bms.price.service.IPayProductService;
|
||||
@@ -59,6 +62,13 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
|
||||
private static final String PAY_TYPE_AR = "AR";
|
||||
|
||||
/** pay_product_term.status:0待生效 1生效 2已失效 */
|
||||
private static final String TERM_PENDING = "0";
|
||||
|
||||
private static final String TERM_EFFECT = "1";
|
||||
|
||||
private static final String TERM_EXPIRED = "2";
|
||||
|
||||
@Autowired
|
||||
private PayProductMapper payProductMapper;
|
||||
|
||||
@@ -71,6 +81,9 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
@Autowired
|
||||
private PayProductTermMapper payProductTermMapper;
|
||||
|
||||
@Autowired
|
||||
private PrcPriceSetMapper priceSetMapper;
|
||||
|
||||
@Autowired
|
||||
private ISysDictTypeService dictTypeService;
|
||||
|
||||
@@ -97,6 +110,21 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
|
||||
@Override
|
||||
public PayPreview preview(byte[] bytes, String filename, String dictValue, String progressId)
|
||||
{
|
||||
return doPreview(bytes, filename, dictValue, progressId, false, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayPreview previewLabel(byte[] bytes, String filename, String dictValue, String progressId, Long priceSetId)
|
||||
{
|
||||
return doPreview(bytes, filename, dictValue, progressId, true, priceSetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param labelMode true=打单套餐:已存在仅看本套餐;false=应付:看公共模板
|
||||
*/
|
||||
private PayPreview doPreview(byte[] bytes, String filename, String dictValue, String progressId,
|
||||
boolean labelMode, Long priceSetId)
|
||||
{
|
||||
String progressKey = progressKey(progressId);
|
||||
PayParseProgressListener listener = progressKey == null ? null : (percent, message) ->
|
||||
@@ -115,10 +143,25 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
for (PayProduct product : preview.getProducts())
|
||||
{
|
||||
product.setFilePath(stored);
|
||||
if (labelMode)
|
||||
{
|
||||
product.setProductType(PAY_TYPE_AR);
|
||||
// 只认「打单侧」产品,绝不把应付公共模板标成已存在
|
||||
Long existsId = isBlank(product.getDictValue()) ? null
|
||||
: payProductMapper.selectLabelProductIdByDict(product.getDictValue());
|
||||
product.setExistsId(existsId);
|
||||
// 仅待生效占用标「已存在」;已生效可继续建新版本
|
||||
boolean pendingInSet = priceSetId != null && existsId != null
|
||||
&& hasLabelTermStatus(priceSetId, existsId, TERM_PENDING);
|
||||
product.setDuplicated(pendingInSet);
|
||||
}
|
||||
else
|
||||
{
|
||||
Long existsId = isBlank(product.getDictValue()) ? null : payProductMapper.selectTemplateId(product.getDictValue());
|
||||
product.setDuplicated(existsId != null);
|
||||
product.setExistsId(existsId);
|
||||
}
|
||||
}
|
||||
writeProgress(progressKey, 100, "解析完成", PayParseProgress.SUCCESS);
|
||||
return preview;
|
||||
}
|
||||
@@ -220,7 +263,6 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
product.setFilePath(resolveSource(product));
|
||||
prepareCounts(product);
|
||||
fitColumns(product);
|
||||
product.setProductId(null);
|
||||
product.setChannelId(null);
|
||||
product.setCreateBy(operator);
|
||||
product.setCreateTime(new Date());
|
||||
@@ -231,13 +273,14 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
}
|
||||
else
|
||||
{
|
||||
product.setProductId(null);
|
||||
product.setOriginId(null);
|
||||
product.setVersionNo(1);
|
||||
payProductMapper.insertPayProduct(product);
|
||||
openTerm(product, false, null);
|
||||
insertBands(product);
|
||||
insertSurcharges(product);
|
||||
insertExtras(product);
|
||||
openTerm(product, false);
|
||||
}
|
||||
saved++;
|
||||
}
|
||||
@@ -425,12 +468,30 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
}
|
||||
|
||||
private void replaceChildren(Long productId, PayProduct product)
|
||||
{
|
||||
product.setProductId(productId);
|
||||
if (product.getTermId() == null)
|
||||
{
|
||||
PayProductTerm term = payProductTermMapper.selectByProductId(productId);
|
||||
if (term != null)
|
||||
{
|
||||
product.setTermId(term.getTermId());
|
||||
}
|
||||
}
|
||||
if (product.getTermId() != null)
|
||||
{
|
||||
List<Long> termIds = Collections.singletonList(product.getTermId());
|
||||
payProductMapper.deleteWeightBandsByTerm(termIds);
|
||||
payProductMapper.deleteSurchargesByTerm(termIds);
|
||||
payProductMapper.deleteExtraLinesByTerm(termIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
List<Long> ids = Collections.singletonList(productId);
|
||||
payProductMapper.deleteWeightBands(ids);
|
||||
payProductMapper.deleteSurcharges(ids);
|
||||
payProductMapper.deleteExtraLines(ids);
|
||||
product.setProductId(productId);
|
||||
}
|
||||
insertBands(product);
|
||||
insertSurcharges(product);
|
||||
insertExtras(product);
|
||||
@@ -511,7 +572,7 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
{
|
||||
term.setDictValue(termKey(product));
|
||||
term.setVersionNo(product.getVersionNo() == null ? 1 : product.getVersionNo());
|
||||
term.setStatus("0");
|
||||
term.setStatus(TERM_EFFECT);
|
||||
term.setCreateBy(operator);
|
||||
payProductTermMapper.insertTerm(term);
|
||||
}
|
||||
@@ -535,9 +596,10 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
@Override
|
||||
public int changeTermStatus(PayProductTerm term)
|
||||
{
|
||||
if (term == null || (!"0".equals(term.getStatus()) && !"1".equals(term.getStatus())))
|
||||
if (term == null || (!TERM_PENDING.equals(term.getStatus()) && !TERM_EFFECT.equals(term.getStatus())
|
||||
&& !TERM_EXPIRED.equals(term.getStatus())))
|
||||
{
|
||||
throw new ServiceException("请选择生效或失效");
|
||||
throw new ServiceException("状态只能是待生效、生效或已失效");
|
||||
}
|
||||
if (term.getTermId() == null && term.getProductId() != null)
|
||||
{
|
||||
@@ -556,27 +618,48 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
}
|
||||
|
||||
/** 给刚保存的这一版记一条生效记录。新版本从生效日开始,旧版本收到生效日前一天。 */
|
||||
private void openTerm(PayProduct product, boolean replacePrevious)
|
||||
private PayProductTerm openTerm(PayProduct product, boolean replacePrevious, Long priceSetId)
|
||||
{
|
||||
String key = termKey(product);
|
||||
if (product.getEffectiveDate() == null)
|
||||
boolean labelMode = priceSetId != null;
|
||||
// 应付:无生效日则默认今天;打单:创建不写默认生效日,由详情「启动」设置
|
||||
if (!labelMode && product.getEffectiveDate() == null)
|
||||
{
|
||||
product.setEffectiveDate(today());
|
||||
}
|
||||
if (replacePrevious)
|
||||
{
|
||||
java.time.LocalDate start = toLocalDate(product.getEffectiveDate());
|
||||
payProductTermMapper.closeOpenTerms(key, toDate(start.minusDays(1)), product.getCreateBy());
|
||||
Date endDate;
|
||||
if (product.getEffectiveDate() != null)
|
||||
{
|
||||
endDate = toDate(toLocalDate(product.getEffectiveDate()).minusDays(1));
|
||||
}
|
||||
else
|
||||
{
|
||||
endDate = toDate(toLocalDate(today()).minusDays(1));
|
||||
}
|
||||
if (labelMode)
|
||||
{
|
||||
payProductTermMapper.closeOpenTermsBySetProduct(priceSetId, product.getProductId(), endDate, product.getCreateBy());
|
||||
}
|
||||
else
|
||||
{
|
||||
payProductTermMapper.closeOpenTerms(key, endDate, product.getCreateBy());
|
||||
}
|
||||
}
|
||||
PayProductTerm term = new PayProductTerm();
|
||||
term.setDictValue(key);
|
||||
term.setProductId(product.getProductId());
|
||||
term.setPriceSetId(priceSetId);
|
||||
term.setVersionNo(product.getVersionNo() == null ? 1 : product.getVersionNo());
|
||||
term.setEffectiveStart(product.getEffectiveDate());
|
||||
term.setEffectiveStart(labelMode ? null : product.getEffectiveDate());
|
||||
term.setFilePath(sourcePath(product.getFilePath()));
|
||||
term.setStatus("0");
|
||||
// 打单套餐新建为待生效,详情启动后再变生效;应付公共模板直接生效
|
||||
term.setStatus(labelMode ? TERM_PENDING : TERM_EFFECT);
|
||||
term.setCreateBy(product.getCreateBy());
|
||||
payProductTermMapper.insertTerm(term);
|
||||
product.setTermId(term.getTermId());
|
||||
return term;
|
||||
}
|
||||
|
||||
private String termKey(PayProduct product)
|
||||
@@ -730,6 +813,20 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PayProductTerm term = payProductTermMapper.selectByProductId(productId);
|
||||
if (term != null)
|
||||
{
|
||||
product.setTermId(term.getTermId());
|
||||
product.setTermStatus(term.getStatus());
|
||||
product.setTermStart(term.getEffectiveStart());
|
||||
product.setTermEnd(term.getEffectiveEnd());
|
||||
product.setVersionNo(term.getVersionNo() == null ? product.getVersionNo() : term.getVersionNo());
|
||||
List<Long> termIds = Collections.singletonList(term.getTermId());
|
||||
product.setWeightBands(payProductMapper.selectWeightBandsByTerm(termIds));
|
||||
product.setSurcharges(payProductMapper.selectSurchargesByTerm(termIds));
|
||||
product.setExtraLines(payProductMapper.selectExtraLinesByTerm(termIds));
|
||||
return product;
|
||||
}
|
||||
List<Long> ids = Collections.singletonList(productId);
|
||||
product.setWeightBands(payProductMapper.selectWeightBands(ids));
|
||||
product.setSurcharges(payProductMapper.selectSurcharges(ids));
|
||||
@@ -737,6 +834,450 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
return product;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayProduct selectPayProductDetailByTerm(Long termId)
|
||||
{
|
||||
if (termId == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PayProductTerm term = payProductTermMapper.selectById(termId);
|
||||
if (term == null || term.getProductId() == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PayProduct product = payProductMapper.selectPayProductById(term.getProductId());
|
||||
if (product == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
product.setTermId(term.getTermId());
|
||||
product.setTermStatus(term.getStatus());
|
||||
product.setTermStart(term.getEffectiveStart());
|
||||
product.setTermEnd(term.getEffectiveEnd());
|
||||
product.setVersionNo(term.getVersionNo() == null ? product.getVersionNo() : term.getVersionNo());
|
||||
if (!isBlank(term.getFilePath()))
|
||||
{
|
||||
product.setFilePath(term.getFilePath());
|
||||
}
|
||||
List<Long> termIds = Collections.singletonList(termId);
|
||||
product.setWeightBands(payProductMapper.selectWeightBandsByTerm(termIds));
|
||||
product.setSurcharges(payProductMapper.selectSurchargesByTerm(termIds));
|
||||
product.setExtraLines(payProductMapper.selectExtraLinesByTerm(termIds));
|
||||
return product;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String saveLabel(PaySaveCommand command)
|
||||
{
|
||||
if (command == null || command.getProducts() == null || command.getProducts().isEmpty())
|
||||
{
|
||||
throw new ServiceException("请先勾选要保存的产品");
|
||||
}
|
||||
Long priceSetId = ensureLabelPriceSet(command);
|
||||
String operator = SecurityUtils.getUsername();
|
||||
Date keepStart = null;
|
||||
String keepStatus = TERM_PENDING;
|
||||
int nextVersionNo;
|
||||
boolean editPendingVersion = command.getVersionNo() != null;
|
||||
if (editPendingVersion)
|
||||
{
|
||||
// 编辑待生效版:清掉该 version 后按原号重写,保留原生效日
|
||||
LabelVersionKeep keep = clearPendingLabelVersion(priceSetId, command.getVersionNo(), operator);
|
||||
keepStart = keep.effectiveStart;
|
||||
keepStatus = keep.status == null ? TERM_PENDING : keep.status;
|
||||
nextVersionNo = command.getVersionNo();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 整批统一套餐级 version_no:新建套餐从 1,新建版本 = max+1
|
||||
Integer maxSetVer = payProductTermMapper.selectMaxVersionByPriceSet(priceSetId);
|
||||
nextVersionNo = (maxSetVer == null ? 0 : maxSetVer) + 1;
|
||||
}
|
||||
int saved = 0;
|
||||
int replaced = 0;
|
||||
int skipped = 0;
|
||||
for (PayProduct product : command.getProducts())
|
||||
{
|
||||
if (product == null || isBlank(product.getProductName()) || isBlank(product.getSheetName()) || isBlank(product.getFileMd5()))
|
||||
{
|
||||
throw new ServiceException("产品名称、工作表和文件摘要不能为空");
|
||||
}
|
||||
product.setProductType(PAY_TYPE_AR);
|
||||
String dict = blankTo(product.getDictValue(), product.getProductName());
|
||||
product.setDictValue(dict);
|
||||
// 独立产品族:只复用打单侧 AR,绝不复用/改写应付公共模板
|
||||
Long existsId = isBlank(dict) ? null : payProductMapper.selectLabelProductIdByDict(dict);
|
||||
// 编辑待生效版时强制覆盖;新建版本仅「待生效」占用时跳过
|
||||
boolean hasPending = !editPendingVersion && existsId != null
|
||||
&& hasLabelTermStatus(priceSetId, existsId, TERM_PENDING);
|
||||
if (hasPending && !command.isOverwrite())
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
product.setFilePath(resolveSource(product));
|
||||
prepareCounts(product);
|
||||
fitColumns(product);
|
||||
product.setChannelId(null);
|
||||
product.setCreateBy(operator);
|
||||
product.setUpdateBy(operator);
|
||||
product.setCreateTime(new Date());
|
||||
product.setVersionNo(nextVersionNo);
|
||||
// 打单不写默认生效日,由详情设置;编辑时下面统一回写 keepStart
|
||||
product.setEffectiveDate(null);
|
||||
if (existsId != null)
|
||||
{
|
||||
appendLabelVersion(existsId, product, operator, priceSetId, nextVersionNo,
|
||||
editPendingVersion || command.isOverwrite());
|
||||
replaced++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 新建打单专用产品行,与应付同名字典产品并存互不影响
|
||||
product.setProductId(null);
|
||||
product.setOriginId(null);
|
||||
payProductMapper.insertPayProduct(product);
|
||||
openTerm(product, false, priceSetId);
|
||||
insertBands(product);
|
||||
insertSurcharges(product);
|
||||
insertExtras(product);
|
||||
}
|
||||
saved++;
|
||||
}
|
||||
if (editPendingVersion && keepStart != null)
|
||||
{
|
||||
payProductTermMapper.updateLabelVersionPeriod(priceSetId, nextVersionNo, keepStart, null, keepStatus, operator);
|
||||
}
|
||||
return editPendingVersion
|
||||
? ("套餐" + priceSetId + "版本V" + nextVersionNo + "已更新" + saved + "个产品")
|
||||
: ("套餐" + priceSetId + "已保存" + saved + "个产品,新版本" + replaced + "个,跳过" + skipped + "个");
|
||||
}
|
||||
|
||||
private static final class LabelVersionKeep
|
||||
{
|
||||
private Date effectiveStart;
|
||||
private String status;
|
||||
}
|
||||
|
||||
/** 仅允许清待生效版;返回原生效日便于编辑后回写 */
|
||||
private LabelVersionKeep clearPendingLabelVersion(Long priceSetId, Integer versionNo, String operator)
|
||||
{
|
||||
if (priceSetId == null || versionNo == null)
|
||||
{
|
||||
throw new ServiceException("请指定要编辑的版本");
|
||||
}
|
||||
List<PayProductTerm> terms = payProductTermMapper.selectByPriceSetIdAndVersion(priceSetId, versionNo);
|
||||
if (terms == null || terms.isEmpty())
|
||||
{
|
||||
throw new ServiceException("该版本不存在或没有产品");
|
||||
}
|
||||
LabelVersionKeep keep = new LabelVersionKeep();
|
||||
List<Long> termIds = new ArrayList<>();
|
||||
for (PayProductTerm term : terms)
|
||||
{
|
||||
if (!TERM_PENDING.equals(term.getStatus()))
|
||||
{
|
||||
// 兼容:生效日未到但 status 误为 1,仍允许编辑
|
||||
boolean futureStart = term.getEffectiveStart() != null
|
||||
&& toLocalDate(term.getEffectiveStart()).isAfter(toLocalDate(today()));
|
||||
if (!futureStart)
|
||||
{
|
||||
throw new ServiceException("只能编辑待生效版本");
|
||||
}
|
||||
}
|
||||
if (keep.effectiveStart == null && term.getEffectiveStart() != null)
|
||||
{
|
||||
keep.effectiveStart = term.getEffectiveStart();
|
||||
}
|
||||
if (keep.status == null)
|
||||
{
|
||||
keep.status = TERM_PENDING;
|
||||
}
|
||||
termIds.add(term.getTermId());
|
||||
}
|
||||
payProductMapper.deleteWeightBandsByTerm(termIds);
|
||||
payProductMapper.deleteSurchargesByTerm(termIds);
|
||||
payProductMapper.deleteExtraLinesByTerm(termIds);
|
||||
payProductTermMapper.deleteByTermIds(termIds);
|
||||
return keep;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PayProduct> selectLabelProducts(Long priceSetId)
|
||||
{
|
||||
return toLabelProducts(payProductTermMapper.selectByPriceSetId(priceSetId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PayProduct> selectLabelProductsByVersion(Long priceSetId, Integer versionNo)
|
||||
{
|
||||
if (priceSetId == null || versionNo == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return toLabelProducts(payProductTermMapper.selectByPriceSetIdAndVersion(priceSetId, versionNo));
|
||||
}
|
||||
|
||||
private List<PayProduct> toLabelProducts(List<PayProductTerm> terms)
|
||||
{
|
||||
if (terms == null || terms.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<PayProduct> list = new ArrayList<>();
|
||||
for (PayProductTerm term : terms)
|
||||
{
|
||||
PayProduct product = payProductMapper.selectPayProductById(term.getProductId());
|
||||
if (product == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
product.setTermId(term.getTermId());
|
||||
product.setTermStatus(term.getStatus());
|
||||
product.setTermStart(term.getEffectiveStart());
|
||||
product.setTermEnd(term.getEffectiveEnd());
|
||||
product.setVersionNo(term.getVersionNo() == null ? product.getVersionNo() : term.getVersionNo());
|
||||
product.setSourceFile(blankTo(term.getSourceFile(), product.getSourceFile()));
|
||||
if (!isBlank(term.getFilePath()))
|
||||
{
|
||||
product.setFilePath(term.getFilePath());
|
||||
}
|
||||
product.setWeightCount(term.getWeightCount() == null ? product.getWeightCount() : term.getWeightCount());
|
||||
product.setSurchargeCount(term.getSurchargeCount() == null ? product.getSurchargeCount() : term.getSurchargeCount());
|
||||
list.add(product);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int removeLabelProducts(Long priceSetId, Long[] productIds)
|
||||
{
|
||||
if (priceSetId == null || productIds == null || productIds.length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
int n = 0;
|
||||
List<PayProductTerm> allTerms = payProductTermMapper.selectByPriceSetId(priceSetId);
|
||||
if (allTerms == null || allTerms.isEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
Set<Long> removeIds = new LinkedHashSet<>(Arrays.asList(productIds));
|
||||
List<Long> termIds = new ArrayList<>();
|
||||
for (PayProductTerm term : allTerms)
|
||||
{
|
||||
if (term.getProductId() != null && removeIds.contains(term.getProductId()))
|
||||
{
|
||||
termIds.add(term.getTermId());
|
||||
}
|
||||
}
|
||||
if (termIds.isEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
payProductMapper.deleteWeightBandsByTerm(termIds);
|
||||
payProductMapper.deleteSurchargesByTerm(termIds);
|
||||
payProductMapper.deleteExtraLinesByTerm(termIds);
|
||||
payProductTermMapper.deleteByTermIds(termIds);
|
||||
return termIds.size();
|
||||
}
|
||||
|
||||
private boolean hasActiveLabelTerm(Long priceSetId, Long productId)
|
||||
{
|
||||
return hasLabelTermStatus(priceSetId, productId, TERM_PENDING)
|
||||
|| hasLabelTermStatus(priceSetId, productId, TERM_EFFECT);
|
||||
}
|
||||
|
||||
private boolean hasLabelTermStatus(Long priceSetId, Long productId, String status)
|
||||
{
|
||||
List<PayProductTerm> terms = payProductTermMapper.selectByPriceSetId(priceSetId);
|
||||
if (terms == null || productId == null || status == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (PayProductTerm term : terms)
|
||||
{
|
||||
if (productId.equals(term.getProductId()) && status.equals(term.getStatus()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打单追加版本:不关闭已生效 term,只在覆盖时关掉待生效;version_no 用套餐级批次号。
|
||||
*/
|
||||
private PayProduct appendLabelVersion(Long rootId, PayProduct incoming, String operator, Long priceSetId,
|
||||
int versionNo, boolean overwritePending)
|
||||
{
|
||||
Long root = resolveRoot(rootId);
|
||||
PayProduct base = payProductMapper.selectPayProductById(root);
|
||||
PayProduct copy = copyHeader(base, incoming == null ? base : incoming);
|
||||
copy.setProductId(root);
|
||||
copy.setOriginId(null);
|
||||
copy.setChannelId(null);
|
||||
copy.setDictValue(blankTo(copy.getDictValue(), base.getDictValue()));
|
||||
copy.setVersionNo(versionNo);
|
||||
copy.setEffectiveDate(null);
|
||||
copy.setCreateBy(operator);
|
||||
copy.setUpdateBy(operator);
|
||||
copy.setCreateTime(new Date());
|
||||
prepareCounts(copy);
|
||||
fitColumns(copy);
|
||||
payProductMapper.updatePayProduct(copy);
|
||||
if (overwritePending)
|
||||
{
|
||||
Date endDate = toDate(toLocalDate(today()).minusDays(1));
|
||||
payProductTermMapper.closePendingBySetProduct(priceSetId, root, endDate, operator);
|
||||
}
|
||||
openTerm(copy, false, priceSetId);
|
||||
insertBands(copy);
|
||||
insertSurcharges(copy);
|
||||
insertExtras(copy);
|
||||
return selectPayProductDetailByTerm(copy.getTermId());
|
||||
}
|
||||
|
||||
private Long ensureLabelPriceSet(PaySaveCommand command)
|
||||
{
|
||||
if (command.getPriceSetId() != null)
|
||||
{
|
||||
PrcPriceSet db = priceSetMapper.selectPrcPriceSetById(command.getPriceSetId());
|
||||
if (db == null)
|
||||
{
|
||||
throw new ServiceException("打单套餐不存在");
|
||||
}
|
||||
if (!PrcPriceSet.CATEGORY_LABEL.equalsIgnoreCase(String.valueOf(db.getSetCategory())))
|
||||
{
|
||||
throw new ServiceException("只能向打单套餐导入价卡");
|
||||
}
|
||||
return db.getPriceSetId();
|
||||
}
|
||||
if (isBlank(command.getPriceSetName()))
|
||||
{
|
||||
throw new ServiceException("请填写打单套餐名称");
|
||||
}
|
||||
String operator = SecurityUtils.getUsername();
|
||||
PrcPriceSet set = new PrcPriceSet();
|
||||
set.setPriceSetName(command.getPriceSetName().trim());
|
||||
set.setPriceSetNote(command.getPriceSetNote());
|
||||
set.setSetType("FIXED");
|
||||
set.setSetCategory(PrcPriceSet.CATEGORY_LABEL);
|
||||
// 新建为待启用,详情页设启动时间后再生效(对齐价格套餐)
|
||||
set.setStatus("D");
|
||||
set.setCreateBy(operator);
|
||||
set.setUpdateBy(operator);
|
||||
priceSetMapper.insertPrcPriceSet(set);
|
||||
set.setPriceSetCode(String.format("LBL%04d", set.getPriceSetId()));
|
||||
priceSetMapper.updatePrcPriceSet(set);
|
||||
command.setPriceSetId(set.getPriceSetId());
|
||||
return set.getPriceSetId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LabelPackageVersion> selectLabelVersions(Long priceSetId)
|
||||
{
|
||||
if (priceSetId == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(priceSetId);
|
||||
if (set == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Date now = today();
|
||||
// 纠正历史:未来生效版启动时把旧版提前标成已失效
|
||||
payProductTermMapper.restorePrematureClosed(priceSetId, now, "system");
|
||||
List<LabelPackageVersion> groups = payProductTermMapper.selectLabelVersionGroups(priceSetId);
|
||||
if (groups == null || groups.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
for (LabelPackageVersion ver : groups)
|
||||
{
|
||||
ver.setPriceSetId(priceSetId);
|
||||
ver.setVersionLabel("V" + (ver.getVersionNo() == null ? 1 : ver.getVersionNo()));
|
||||
int active = ver.getActiveCount() == null ? 0 : ver.getActiveCount();
|
||||
int pending = ver.getPendingCount() == null ? 0 : ver.getPendingCount();
|
||||
Date start = ver.getEffectiveTime();
|
||||
Date end = ver.getExpireTime();
|
||||
// 按日期判定:未到生效日=待生效;已过失效日=已失效;中间段=生效
|
||||
if (start == null)
|
||||
{
|
||||
ver.setStatus(pending > 0 ? TERM_PENDING : (active > 0 ? TERM_EFFECT : TERM_EXPIRED));
|
||||
}
|
||||
else if (toLocalDate(start).isAfter(toLocalDate(now)))
|
||||
{
|
||||
ver.setStatus(TERM_PENDING);
|
||||
}
|
||||
else if (end != null && toLocalDate(end).isBefore(toLocalDate(now)))
|
||||
{
|
||||
ver.setStatus(TERM_EXPIRED);
|
||||
}
|
||||
else
|
||||
{
|
||||
ver.setStatus(TERM_EFFECT);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String activateLabelVersion(Long priceSetId, Integer versionNo, Date effectiveTime)
|
||||
{
|
||||
if (priceSetId == null || versionNo == null)
|
||||
{
|
||||
throw new ServiceException("请选择要启动的版本");
|
||||
}
|
||||
if (effectiveTime == null)
|
||||
{
|
||||
throw new ServiceException("请选择生效时间");
|
||||
}
|
||||
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(priceSetId);
|
||||
if (set == null)
|
||||
{
|
||||
throw new ServiceException("打单套餐不存在");
|
||||
}
|
||||
if (!PrcPriceSet.CATEGORY_LABEL.equalsIgnoreCase(String.valueOf(set.getSetCategory())))
|
||||
{
|
||||
throw new ServiceException("只能启动打单套餐版本");
|
||||
}
|
||||
String operator = SecurityUtils.getUsername();
|
||||
boolean future = toLocalDate(effectiveTime).isAfter(toLocalDate(today()));
|
||||
Date endDate = toDate(toLocalDate(effectiveTime).minusDays(1));
|
||||
if (future)
|
||||
{
|
||||
// 未到生效日:本版保持待生效;旧版只预约失效日,当前仍生效
|
||||
payProductTermMapper.scheduleEndExceptVersion(priceSetId, versionNo, endDate, today(), operator);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 当天或已过:本版立刻生效,旧版关掉
|
||||
payProductTermMapper.closeEffectExceptVersion(priceSetId, versionNo, endDate, operator);
|
||||
}
|
||||
String termStatus = future ? TERM_PENDING : TERM_EFFECT;
|
||||
int n = payProductTermMapper.updateLabelVersionPeriod(priceSetId, versionNo, effectiveTime, null, termStatus, operator);
|
||||
if (n <= 0)
|
||||
{
|
||||
throw new ServiceException("该版本没有可启动的产品价卡");
|
||||
}
|
||||
// 真正生效时才把套餐壳置为生效
|
||||
if (!future && !"E".equalsIgnoreCase(String.valueOf(set.getStatus())))
|
||||
{
|
||||
PrcPriceSet upd = new PrcPriceSet();
|
||||
upd.setPriceSetId(priceSetId);
|
||||
upd.setStatus("E");
|
||||
upd.setUpdateBy(operator);
|
||||
priceSetMapper.updatePrcPriceSetStatus(upd);
|
||||
}
|
||||
return future ? "已设为待生效" : "版本已启动";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deletePayProducts(Long[] productIds)
|
||||
@@ -826,6 +1367,7 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
for (PayWeightBand row : rows)
|
||||
{
|
||||
row.setProductId(product.getProductId());
|
||||
row.setTermId(product.getTermId());
|
||||
row.setVersionNo(product.getVersionNo() == null ? 1 : product.getVersionNo());
|
||||
}
|
||||
for (int i = 0; i < rows.size(); i += BATCH)
|
||||
@@ -844,6 +1386,7 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
for (PaySurcharge row : rows)
|
||||
{
|
||||
row.setProductId(product.getProductId());
|
||||
row.setTermId(product.getTermId());
|
||||
row.setVersionNo(product.getVersionNo() == null ? 1 : product.getVersionNo());
|
||||
}
|
||||
for (int i = 0; i < rows.size(); i += BATCH)
|
||||
@@ -868,6 +1411,7 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
for (PayExtraLine row : rows)
|
||||
{
|
||||
row.setProductId(product.getProductId());
|
||||
row.setTermId(product.getTermId());
|
||||
}
|
||||
for (int i = 0; i < rows.size(); i += BATCH)
|
||||
{
|
||||
@@ -938,7 +1482,7 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindChannel(String channelId, Long productId, Long priceSetId, String payType)
|
||||
public void bindChannel(String channelId, Long productId, String payType)
|
||||
{
|
||||
if (isBlank(channelId) || productId == null)
|
||||
{
|
||||
@@ -955,7 +1499,6 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
row.setChannelId(channelId);
|
||||
row.setChannelCode(channel.getChannelCode());
|
||||
row.setProductId(storedId);
|
||||
row.setPriceSetId(priceSetId);
|
||||
row.setPayType(type);
|
||||
String operator = SecurityUtils.getUsername();
|
||||
row.setCreateBy(operator);
|
||||
@@ -994,6 +1537,36 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayProduct selectLabelPriceForDate(Long priceSetId, String channelId, Date bizDate)
|
||||
{
|
||||
if (priceSetId == null)
|
||||
{
|
||||
throw new ServiceException("打单套餐不能为空");
|
||||
}
|
||||
if (isBlank(channelId))
|
||||
{
|
||||
throw new ServiceException("打单计费需要渠道");
|
||||
}
|
||||
Long root = channelProductRoot(channelId.trim(), PAY_TYPE_AR);
|
||||
if (root == null)
|
||||
{
|
||||
throw new ServiceException("渠道还没有绑定打单应收产品");
|
||||
}
|
||||
Date day = bizDate == null ? today() : bizDate;
|
||||
Long termId = payProductTermMapper.selectTermIdBySetProductAt(priceSetId, root, day);
|
||||
if (termId == null)
|
||||
{
|
||||
throw new ServiceException("订单时间没有落在打单价卡有效期内");
|
||||
}
|
||||
PayProduct product = selectPayProductDetailByTerm(termId);
|
||||
if (product == null)
|
||||
{
|
||||
throw new ServiceException("打单价卡版本不存在");
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayPriceCompare compare(Long leftId, Long rightId)
|
||||
{
|
||||
@@ -1015,37 +1588,42 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
return compared;
|
||||
}
|
||||
|
||||
/** 在同一个产品编号下追加一版价格,不新开产品编号。 */
|
||||
/** 在同一个产品编号下追加一版价格:复用稳定 product_id,明细挂新 term_id。 */
|
||||
private PayProduct appendVersion(Long rootId, PayProduct incoming, String operator)
|
||||
{
|
||||
return appendVersion(rootId, incoming, operator, null);
|
||||
}
|
||||
|
||||
private PayProduct appendVersion(Long rootId, PayProduct incoming, String operator, Long priceSetId)
|
||||
{
|
||||
Long root = resolveRoot(rootId);
|
||||
PayProduct base = payProductMapper.selectPayProductById(root);
|
||||
PayProduct copy = copyHeader(base, incoming == null ? base : incoming);
|
||||
copy.setProductId(null);
|
||||
copy.setOriginId(root);
|
||||
copy.setProductId(root);
|
||||
copy.setOriginId(null);
|
||||
copy.setChannelId(null);
|
||||
copy.setDictValue(blankTo(copy.getDictValue(), base.getDictValue()));
|
||||
Integer maxVersion = payProductMapper.selectMaxVersion(root);
|
||||
Integer maxVersion = payProductTermMapper.selectMaxVersionByProduct(root, priceSetId);
|
||||
if (maxVersion == null || maxVersion < 1)
|
||||
{
|
||||
maxVersion = payProductMapper.selectMaxVersion(root);
|
||||
}
|
||||
copy.setVersionNo((maxVersion == null ? 1 : maxVersion) + 1);
|
||||
if (copy.getEffectiveDate() == null)
|
||||
{
|
||||
copy.setEffectiveDate(today());
|
||||
}
|
||||
copy.setCreateBy(operator);
|
||||
copy.setUpdateBy(operator);
|
||||
copy.setCreateTime(new Date());
|
||||
prepareCounts(copy);
|
||||
fitColumns(copy);
|
||||
payProductMapper.insertPayProduct(copy);
|
||||
payProductMapper.updatePayProduct(copy);
|
||||
openTerm(copy, true, priceSetId);
|
||||
insertBands(copy);
|
||||
insertSurcharges(copy);
|
||||
insertExtras(copy);
|
||||
openTerm(copy, true);
|
||||
PayProduct saved = selectPayProductDetail(copy.getProductId());
|
||||
if (saved != null)
|
||||
{
|
||||
saved.setOriginId(root);
|
||||
}
|
||||
return saved;
|
||||
return selectPayProductDetailByTerm(copy.getTermId());
|
||||
}
|
||||
|
||||
/** 渠道产品绑定记在映射表,产品编号是不变的根产品。 */
|
||||
|
||||
+10
-2
@@ -77,6 +77,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
private static final String TYPE_FIXED = "FIXED";
|
||||
private static final String CATEGORY_PACKAGE = PrcPriceSet.CATEGORY_PACKAGE;
|
||||
private static final String CATEGORY_EXCLUSIVE = PrcPriceSet.CATEGORY_EXCLUSIVE;
|
||||
private static final String CATEGORY_LABEL = PrcPriceSet.CATEGORY_LABEL;
|
||||
private static final String TYPE_DISCOUNT = "DISCOUNT";
|
||||
private static final String VER_DRAFT = "0";
|
||||
private static final String VER_EFFECT = "1";
|
||||
@@ -2979,16 +2980,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
/** PACKAGE (default) or EXCLUSIVE. */
|
||||
public static String normalizeSetCategory(String setCategory)
|
||||
{
|
||||
if (CATEGORY_EXCLUSIVE.equalsIgnoreCase(StringUtils.trim(setCategory)))
|
||||
String raw = StringUtils.trim(setCategory);
|
||||
if (CATEGORY_EXCLUSIVE.equalsIgnoreCase(raw))
|
||||
{
|
||||
return CATEGORY_EXCLUSIVE;
|
||||
}
|
||||
if (CATEGORY_LABEL.equalsIgnoreCase(raw))
|
||||
{
|
||||
return CATEGORY_LABEL;
|
||||
}
|
||||
return CATEGORY_PACKAGE;
|
||||
}
|
||||
|
||||
private static String buildPriceSetCode(String setCategory, Long priceSetId)
|
||||
{
|
||||
String prefix = CATEGORY_EXCLUSIVE.equals(normalizeSetCategory(setCategory)) ? "EXC" : "SET";
|
||||
String category = normalizeSetCategory(setCategory);
|
||||
String prefix = CATEGORY_EXCLUSIVE.equals(category) ? "EXC"
|
||||
: (CATEGORY_LABEL.equals(category) ? "LBL" : "SET");
|
||||
return String.format("%s%04d", prefix, priceSetId);
|
||||
}
|
||||
|
||||
|
||||
+200
-3
@@ -28,7 +28,11 @@ import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||
import com.bms.price.domain.BmsProduct;
|
||||
import com.bms.price.domain.BmsProductCountry;
|
||||
import com.bms.price.domain.BmsRedlineProductCountry;
|
||||
import com.bms.price.domain.PayCostLine;
|
||||
import com.bms.price.domain.PayOrderCostResult;
|
||||
import com.bms.price.domain.PayProduct;
|
||||
import com.bms.price.domain.PrcCustomerPriceSet;
|
||||
import com.bms.price.domain.PrcOrderBillExt;
|
||||
import com.bms.price.domain.PrcPriceSet;
|
||||
import com.bms.price.domain.PrcPriceSetDetail;
|
||||
import com.bms.price.domain.PrcPriceSetProduct;
|
||||
@@ -37,6 +41,7 @@ import com.bms.price.domain.PrcQuote;
|
||||
import com.bms.price.domain.PrcQuoteFee;
|
||||
import com.bms.price.domain.PrcSurchargeRel;
|
||||
import com.bms.price.domain.PrcZone;
|
||||
import com.bms.price.pay.PayCostCalculator;
|
||||
import com.bms.price.mapper.PrcZoneDetailMapper;
|
||||
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteFeeLine;
|
||||
@@ -48,6 +53,7 @@ import com.bms.price.mapper.BmsProductCountryMapper;
|
||||
import com.bms.price.mapper.BmsProductMapper;
|
||||
import com.bms.price.mapper.BmsRedlineProductCountryMapper;
|
||||
import com.bms.price.mapper.PrcCustomerPriceSetMapper;
|
||||
import com.bms.price.mapper.PrcOrderBillExtMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetDetailMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetProductMapper;
|
||||
@@ -58,12 +64,14 @@ import com.bms.price.mapper.PrcSurchargeRelMapper;
|
||||
import com.bms.price.search.EsBizLogSupport;
|
||||
import com.bms.price.search.EsLogType;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.service.IPayProductService;
|
||||
import com.bms.price.service.ITmsPriceQuoteService;
|
||||
import com.bms.system.service.IBmsProductCodeMappingService;
|
||||
|
||||
/**
|
||||
* 共用的试算/计费计算:按 bizDate 取客户绑定和套餐版本(版本 effective_time)。
|
||||
* 绑定的 pr_begindate/pr_enddate 不作为计费时间门槛。
|
||||
* 先按 prc_price_set.set_category 分流:LABEL 走打单 pay_*;PACKAGE/EXCLUSIVE 走销售快照。
|
||||
* 销售价只来自套餐快照(FIXED 明细 / 按目录底价 DISCOUNT)。
|
||||
* 无绑定或无生效版本:试算和计费都直接失败(不用 PRODUCT_DEFAULT)。
|
||||
* 账单同步时,产品/国家/重量段未命中为软失败(billing_fail_status 1/2/3,金额为 0)。
|
||||
@@ -78,6 +86,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
private static final String TYPE_DISCOUNT = "DISCOUNT";
|
||||
private static final String SRC_FIXED = "PACKAGE_FIXED";
|
||||
private static final String SRC_DISCOUNT = "PACKAGE_DISCOUNT";
|
||||
private static final String SRC_LABEL = "LABEL_PAY";
|
||||
|
||||
private static final String FEE_BASIC = "BASIC_FREIGHT";
|
||||
private static final String FEE_REG = "REGISTRATION_FEE";
|
||||
@@ -128,6 +137,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
@Autowired
|
||||
private BmsCustomerMapper bmsCustomerMapper;
|
||||
|
||||
@Autowired
|
||||
private IPayProductService payProductService;
|
||||
|
||||
@Autowired
|
||||
private PrcOrderBillExtMapper orderBillExtMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public TmsPriceQuoteResult quote(TmsPriceQuoteRequest request)
|
||||
@@ -169,7 +184,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return toResult(cached, fees, true);
|
||||
}
|
||||
TmsPriceQuoteResult refreshed = doQuote(request, bizDate);
|
||||
applySurchargesAndFees(refreshed);
|
||||
applySalesSurchargesIfNeeded(refreshed);
|
||||
replaceQuoteFees(cached.getQuoteId(), refreshed);
|
||||
refreshed.setQuoteId(cached.getQuoteId());
|
||||
refreshed.setFromCache(false);
|
||||
@@ -179,11 +194,21 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
TmsPriceQuoteResult result = doQuote(request, bizDate);
|
||||
applySurchargesAndFees(result);
|
||||
applySalesSurchargesIfNeeded(result);
|
||||
persistQuote(quoteKey, request, weight, bizDate, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** LABEL 费用已由 PayCostCalculator 算完,不再叠加销售附加费。 */
|
||||
private void applySalesSurchargesIfNeeded(TmsPriceQuoteResult result)
|
||||
{
|
||||
if (result == null || SRC_LABEL.equals(result.getMatchSource()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
applySurchargesAndFees(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached quotes from pre-package-snapshot logic (PRODUCT_DEFAULT / missing package id)
|
||||
* must not be reused after billing rules changed.
|
||||
@@ -211,7 +236,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
TmsPriceQuoteResult result = doQuote(request, bizDate);
|
||||
if (!BillBillingFailStatus.isFail(result.getBillingFailStatus()))
|
||||
{
|
||||
applySurchargesAndFees(result);
|
||||
applySalesSurchargesIfNeeded(result);
|
||||
result.setBillingFailStatus(BillBillingFailStatus.SUCCESS.getCode());
|
||||
}
|
||||
result.setCustomerActiveStatus(customerActiveStatus);
|
||||
@@ -442,6 +467,13 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
continue;
|
||||
}
|
||||
anyEnabledSet = true;
|
||||
// 打单套餐:独立 pay_* 链路,不走销售版本明细
|
||||
if (PrcPriceSet.CATEGORY_LABEL.equals(
|
||||
PrcPriceSetServiceImpl.normalizeSetCategory(set.getSetCategory())))
|
||||
{
|
||||
return doLabelQuote(request, bind, set, productCode, productName, countryCode, countryName,
|
||||
weight, bizDate);
|
||||
}
|
||||
// Temporal gate: prc_price_set_version.effective_time / expire_time (not bind pr_begindate)
|
||||
PrcPriceSetVersion version = versionMapper.selectActiveByPriceSetAt(set.getPriceSetId(), asOf);
|
||||
if (version == null || version.getPsvId() == null)
|
||||
@@ -622,6 +654,171 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
throw new ServiceException(MessageUtils.message(missKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* 打单套餐计费:渠道 AR → product_id,套餐+时间 → term,PayCostCalculator 算费。
|
||||
*/
|
||||
private TmsPriceQuoteResult doLabelQuote(TmsPriceQuoteRequest request, PrcCustomerPriceSet bind,
|
||||
PrcPriceSet set, String productCode, String productName, String countryCode, String countryName,
|
||||
BigDecimal weight, Date bizDate)
|
||||
{
|
||||
String channelId = resolveLabelChannelId(request);
|
||||
if (StringUtils.isEmpty(channelId))
|
||||
{
|
||||
if (request instanceof TmsPriceBillRequest)
|
||||
{
|
||||
return buildBillingMissResult(request, productCode, productName, countryCode, countryName,
|
||||
weight, bizDate, BillBillingFailStatus.PRICE_NOT_HIT.getCode());
|
||||
}
|
||||
throw new ServiceException("打单计费需要渠道");
|
||||
}
|
||||
PayProduct product;
|
||||
try
|
||||
{
|
||||
product = payProductService.selectLabelPriceForDate(set.getPriceSetId(), channelId, bizDate);
|
||||
}
|
||||
catch (ServiceException ex)
|
||||
{
|
||||
if (request instanceof TmsPriceBillRequest)
|
||||
{
|
||||
return buildBillingMissResult(request, productCode, productName, countryCode, countryName,
|
||||
weight, bizDate, BillBillingFailStatus.NO_ACTIVE_PACKAGE_VERSION.getCode());
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
BigDecimal length = null;
|
||||
BigDecimal width = null;
|
||||
BigDecimal height = null;
|
||||
Integer orderStatus = null;
|
||||
if (request instanceof TmsPriceBillRequest)
|
||||
{
|
||||
TmsPriceBillRequest billReq = (TmsPriceBillRequest) request;
|
||||
length = billReq.getPkgLength();
|
||||
width = billReq.getPkgWidth();
|
||||
height = billReq.getPkgHeight();
|
||||
orderStatus = billReq.getOrderStatus();
|
||||
}
|
||||
PayOrderCostResult cost = PayCostCalculator.calculate(product, weight, length, width, height,
|
||||
request.getDestPostcode(), bizDate, orderStatus);
|
||||
if (cost == null || cost.getFees() == null || cost.getFees().isEmpty())
|
||||
{
|
||||
if (request instanceof TmsPriceBillRequest)
|
||||
{
|
||||
return buildBillingMissResult(request, productCode, productName, countryCode, countryName,
|
||||
weight, bizDate, BillBillingFailStatus.PRICE_NOT_HIT.getCode());
|
||||
}
|
||||
throw new ServiceException(cost != null && StringUtils.isNotEmpty(cost.getMessage())
|
||||
? cost.getMessage() : "打单价卡未算出费用");
|
||||
}
|
||||
TmsPriceQuoteResult r = mapLabelCostToQuote(cost, product, request, bind, set,
|
||||
productCode, productName, countryCode, countryName, weight, bizDate);
|
||||
stampRequestMeta(request, r);
|
||||
return r;
|
||||
}
|
||||
|
||||
/** 试算用 request.channelId;计费优先订单扩展 productChannelId。 */
|
||||
private String resolveLabelChannelId(TmsPriceQuoteRequest request)
|
||||
{
|
||||
if (request != null && StringUtils.isNotEmpty(request.getChannelId()))
|
||||
{
|
||||
return request.getChannelId().trim();
|
||||
}
|
||||
if (!(request instanceof TmsPriceBillRequest))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
TmsPriceBillRequest billReq = (TmsPriceBillRequest) request;
|
||||
PrcOrderBillExt ext = null;
|
||||
if (StringUtils.isNotEmpty(billReq.getOrderNo()))
|
||||
{
|
||||
ext = orderBillExtMapper.selectByOrderNo(billReq.getOrderNo().trim());
|
||||
}
|
||||
if (ext == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotEmpty(ext.getProductChannelId()))
|
||||
{
|
||||
return ext.getProductChannelId().trim();
|
||||
}
|
||||
if (StringUtils.isNotEmpty(ext.getProductFloorCode()))
|
||||
{
|
||||
return ext.getProductFloorCode().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private TmsPriceQuoteResult mapLabelCostToQuote(PayOrderCostResult cost, PayProduct product,
|
||||
TmsPriceQuoteRequest request, PrcCustomerPriceSet bind, PrcPriceSet set,
|
||||
String productCode, String productName, String countryCode, String countryName,
|
||||
BigDecimal weight, Date bizDate)
|
||||
{
|
||||
RoundingMode rm = RoundingMode.HALF_UP;
|
||||
String currency = normalizeCurrency(cost.getCurrency());
|
||||
List<TmsPriceQuoteFeeLine> lines = new ArrayList<TmsPriceQuoteFeeLine>();
|
||||
BigDecimal freightAmount = BigDecimal.ZERO;
|
||||
BigDecimal surchargeAmount = BigDecimal.ZERO;
|
||||
BigDecimal registrationFee = BigDecimal.ZERO;
|
||||
for (PayCostLine fee : cost.getFees())
|
||||
{
|
||||
if (fee == null || fee.getAmount() == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String feeType = StringUtils.isEmpty(fee.getFeeType()) ? FEE_SUR : fee.getFeeType().trim();
|
||||
String feeName = StringUtils.isEmpty(fee.getFeeName()) ? feeNameOf(feeType) : fee.getFeeName();
|
||||
BigDecimal amount = fee.getAmount().setScale(2, rm);
|
||||
Long scId = fee.getScId() == null ? 0L : fee.getScId();
|
||||
lines.add(feeLine(feeType, feeName, scId, amount, currency, "HALF_UP", fee.getRemark()));
|
||||
if (FEE_BASIC.equals(feeType))
|
||||
{
|
||||
freightAmount = freightAmount.add(amount);
|
||||
}
|
||||
else if (FEE_REG.equals(feeType))
|
||||
{
|
||||
registrationFee = registrationFee.add(amount);
|
||||
}
|
||||
else
|
||||
{
|
||||
surchargeAmount = surchargeAmount.add(amount);
|
||||
}
|
||||
}
|
||||
BigDecimal incomeTotal = nvl(cost.getPayTotal());
|
||||
if (incomeTotal.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
incomeTotal = freightAmount.add(registrationFee).add(surchargeAmount).setScale(2, rm);
|
||||
}
|
||||
String labelProductName = StringUtils.isNotEmpty(product.getProductName())
|
||||
? product.getProductName() : productName;
|
||||
TmsPriceQuoteResult r = new TmsPriceQuoteResult();
|
||||
r.setMatchSource(SRC_LABEL);
|
||||
r.setMatchMessage("hit label package pay card V"
|
||||
+ (product.getVersionNo() == null ? "?" : product.getVersionNo()));
|
||||
fillPackage(r, bind, set);
|
||||
r.setVersionNo(product.getVersionNo() == null ? null : ("V" + product.getVersionNo()));
|
||||
r.setCustomerId(bind.getCustomerId());
|
||||
r.setProductCode(productCode);
|
||||
r.setProductName(labelProductName);
|
||||
r.setCountryCode(countryCode);
|
||||
r.setCountryName(countryName);
|
||||
r.setWeight(weight);
|
||||
r.setChargeWeight(weight);
|
||||
r.setBizDate(bizDate);
|
||||
r.setEffectiveDate(product.getTermStart() != null ? product.getTermStart() : product.getEffectiveDate());
|
||||
r.setFreightRate(weight != null && weight.compareTo(BigDecimal.ZERO) > 0
|
||||
? freightAmount.divide(weight, 2, rm) : freightAmount);
|
||||
r.setFreightAmount(freightAmount.setScale(2, rm));
|
||||
r.setRegistrationFee(registrationFee.setScale(2, rm));
|
||||
r.setSurchargeAmount(surchargeAmount.setScale(2, rm));
|
||||
r.setPayableTotal(incomeTotal);
|
||||
r.setIncomeTotal(incomeTotal);
|
||||
r.setCurrency(currency);
|
||||
r.setRoundMode("HALF_UP");
|
||||
r.setFromCache(false);
|
||||
r.setFeeDetails(lines);
|
||||
r.setPayFeeDetails(Collections.<TmsPriceQuoteFeeLine>emptyList());
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnose FIXED miss: product rows absent -> 1; country/zone absent -> 2; else weight -> 3.
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="selectByChannelAndType" resultType="com.bms.price.domain.ChannelProductMap">
|
||||
select id, channel_id as channelId, channel_code as channelCode, product_id as productId,
|
||||
price_set_id as priceSetId, pay_type as payType, create_by as createBy, create_time as createTime,
|
||||
pay_type as payType, create_by as createBy, create_time as createTime,
|
||||
update_by as updateBy, update_time as updateTime
|
||||
from srv_channel_product_map
|
||||
where channel_id = #{channelId} and pay_type = #{payType}
|
||||
@@ -15,14 +15,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<insert id="upsert">
|
||||
insert into srv_channel_product_map (
|
||||
channel_id, channel_code, product_id, price_set_id, pay_type, create_by, create_time, update_by, update_time
|
||||
channel_id, channel_code, product_id, pay_type, create_by, create_time, update_by, update_time
|
||||
) values (
|
||||
#{channelId}, #{channelCode}, #{productId}, #{priceSetId}, #{payType}, #{createBy}, sysdate(), #{updateBy}, sysdate()
|
||||
#{channelId}, #{channelCode}, #{productId}, #{payType}, #{createBy}, sysdate(), #{updateBy}, sysdate()
|
||||
)
|
||||
on duplicate key update
|
||||
channel_code = values(channel_code),
|
||||
product_id = values(product_id),
|
||||
price_set_id = values(price_set_id),
|
||||
update_by = values(update_by),
|
||||
update_time = sysdate()
|
||||
</insert>
|
||||
|
||||
@@ -49,6 +49,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<where>
|
||||
<if test="templateOnly != null and templateOnly">
|
||||
and (pay_product.channel_id is null or pay_product.channel_id = '')
|
||||
and not (
|
||||
exists (
|
||||
select 1 from pay_product_term t_label
|
||||
where t_label.product_id = pay_product.product_id
|
||||
and t_label.price_set_id is not null
|
||||
)
|
||||
and not exists (
|
||||
select 1 from pay_product_term t_pay
|
||||
where t_pay.product_id = pay_product.product_id
|
||||
and t_pay.price_set_id is null
|
||||
)
|
||||
)
|
||||
</if>
|
||||
<if test="currentOnly != null and currentOnly">
|
||||
and pay_product.origin_id is null
|
||||
@@ -129,7 +141,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
inner join pay_product v on v.product_id = t2.product_id
|
||||
where (v.channel_id is null or v.channel_id = '')
|
||||
and (v.product_id = pay_product.product_id or v.origin_id = pay_product.product_id)
|
||||
order by case when t2.status = '0' then 0 else 1 end, ifnull(v.version_no, 1) desc, t2.term_id desc
|
||||
and t2.price_set_id is null
|
||||
order by case when t2.status = '1' then 0 when t2.status = '0' then 1 else 2 end, ifnull(v.version_no, 1) desc, t2.term_id desc
|
||||
limit 1
|
||||
)
|
||||
left join pay_product cur on cur.product_id = t.product_id
|
||||
@@ -202,15 +215,65 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where product_id = #{productId}
|
||||
</update>
|
||||
|
||||
<!-- 应付公共模板:排除「仅打单套餐」产品(有 label term、无公共 term) -->
|
||||
<sql id="payPublicTemplateOnly">
|
||||
and not (
|
||||
exists (
|
||||
select 1 from pay_product_term t_label
|
||||
where t_label.product_id = pay_product.product_id
|
||||
and t_label.price_set_id is not null
|
||||
)
|
||||
and not exists (
|
||||
select 1 from pay_product_term t_pay
|
||||
where t_pay.product_id = pay_product.product_id
|
||||
and t_pay.price_set_id is null
|
||||
)
|
||||
)
|
||||
</sql>
|
||||
|
||||
<select id="selectTemplateId" resultType="long">
|
||||
select product_id from pay_product
|
||||
where dict_value = #{dictValue}
|
||||
and (channel_id is null or channel_id = '')
|
||||
and origin_id is null
|
||||
<include refid="payPublicTemplateOnly"/>
|
||||
order by product_id
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectTemplateIdByType" resultType="long">
|
||||
select product_id from pay_product
|
||||
where dict_value = #{dictValue}
|
||||
and ifnull(product_type, 'AP') = #{productType}
|
||||
and (channel_id is null or channel_id = '')
|
||||
and origin_id is null
|
||||
<include refid="payPublicTemplateOnly"/>
|
||||
order by product_id
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!-- 打单套餐专用:只命中已有 label term 的 AR 产品,绝不碰应付公共模板 -->
|
||||
<select id="selectLabelProductIdByDict" resultType="long">
|
||||
select p.product_id
|
||||
from pay_product p
|
||||
where p.dict_value = #{dictValue}
|
||||
and ifnull(p.product_type, 'AP') = 'AR'
|
||||
and (p.channel_id is null or p.channel_id = '')
|
||||
and p.origin_id is null
|
||||
and exists (
|
||||
select 1 from pay_product_term t
|
||||
where t.product_id = p.product_id
|
||||
and t.price_set_id is not null
|
||||
)
|
||||
and not exists (
|
||||
select 1 from pay_product_term t2
|
||||
where t2.product_id = p.product_id
|
||||
and t2.price_set_id is null
|
||||
)
|
||||
order by p.product_id
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectTemplateOptions" resultMap="PayProductResult">
|
||||
select p.product_id, p.product_name, p.product_type, p.dict_value, p.sheet_name, p.supplier_name, p.effective_date,
|
||||
p.currency, ifnull(cur.version_no, p.version_no) as version_no
|
||||
@@ -221,13 +284,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
inner join pay_product v on v.product_id = t2.product_id
|
||||
where (v.channel_id is null or v.channel_id = '')
|
||||
and (v.product_id = p.product_id or v.origin_id = p.product_id)
|
||||
order by case when t2.status = '0' then 0 else 1 end, ifnull(v.version_no, 1) desc, t2.term_id desc
|
||||
and t2.price_set_id is null
|
||||
order by case when t2.status = '1' then 0 when t2.status = '0' then 1 else 2 end, ifnull(v.version_no, 1) desc, t2.term_id desc
|
||||
limit 1
|
||||
)
|
||||
left join pay_product cur on cur.product_id = t.product_id
|
||||
where (p.channel_id is null or p.channel_id = '')
|
||||
and p.origin_id is null
|
||||
and ifnull(p.product_type, 'AP') = #{payType}
|
||||
and not (
|
||||
exists (
|
||||
select 1 from pay_product_term t_label
|
||||
where t_label.product_id = p.product_id
|
||||
and t_label.price_set_id is not null
|
||||
)
|
||||
and not exists (
|
||||
select 1 from pay_product_term t_pay
|
||||
where t_pay.product_id = p.product_id
|
||||
and t_pay.price_set_id is null
|
||||
)
|
||||
)
|
||||
order by p.product_name
|
||||
</select>
|
||||
|
||||
@@ -238,11 +314,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<insert id="insertWeightBands">
|
||||
insert into pay_weight_band (
|
||||
product_id, version_no, section_name, country_name, country_code, zone_name, weight_label, weight_min, weight_max,
|
||||
product_id, term_id, version_no, section_name, country_name, country_code, zone_name, weight_label, weight_min, weight_max,
|
||||
volume_text, charge_name, price, price_unit, sort_no
|
||||
) values
|
||||
<foreach collection="list" item="row" separator=",">
|
||||
(#{row.productId}, ifnull(#{row.versionNo}, 1), #{row.sectionName}, #{row.countryName}, #{row.countryCode}, #{row.zoneName},
|
||||
(#{row.productId}, #{row.termId}, ifnull(#{row.versionNo}, 1), #{row.sectionName}, #{row.countryName}, #{row.countryCode}, #{row.zoneName},
|
||||
#{row.weightLabel}, #{row.weightMin}, #{row.weightMax}, #{row.volumeText}, #{row.chargeName}, #{row.price},
|
||||
#{row.priceUnit}, #{row.sortNo})
|
||||
</foreach>
|
||||
@@ -250,11 +326,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<insert id="insertSurcharges">
|
||||
insert into pay_surcharge (
|
||||
product_id, version_no, section_name, fee_name, zone_name, charge_text, amount, currency, price_unit, fee_desc, period_text,
|
||||
product_id, term_id, version_no, section_name, fee_name, zone_name, charge_text, amount, currency, price_unit, fee_desc, period_text,
|
||||
effective_start, effective_end, charge_timing, calc_rule, calc_param, weight_min, weight_max, postcode_param, sort_no
|
||||
) values
|
||||
<foreach collection="list" item="row" separator=",">
|
||||
(#{row.productId}, ifnull(#{row.versionNo}, 1), #{row.sectionName}, #{row.feeName}, #{row.zoneName}, #{row.chargeText}, #{row.amount},
|
||||
(#{row.productId}, #{row.termId}, ifnull(#{row.versionNo}, 1), #{row.sectionName}, #{row.feeName}, #{row.zoneName}, #{row.chargeText}, #{row.amount},
|
||||
#{row.currency}, #{row.priceUnit}, #{row.feeDesc}, #{row.periodText}, #{row.effectiveStart}, #{row.effectiveEnd},
|
||||
#{row.chargeTiming}, ifnull(#{row.calcRule}, 'MANUAL'), #{row.calcParam}, #{row.weightMin}, #{row.weightMax},
|
||||
#{row.postcodeParam}, #{row.sortNo})
|
||||
@@ -262,14 +338,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</insert>
|
||||
|
||||
<insert id="insertExtraLines">
|
||||
insert into pay_extra_line (product_id, line_type, line_text, sort_no) values
|
||||
insert into pay_extra_line (product_id, term_id, line_type, line_text, sort_no) values
|
||||
<foreach collection="list" item="row" separator=",">
|
||||
(#{row.productId}, #{row.lineType}, #{row.lineText}, #{row.sortNo})
|
||||
(#{row.productId}, #{row.termId}, #{row.lineType}, #{row.lineText}, #{row.sortNo})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<select id="selectWeightBands" resultType="com.bms.price.domain.PayWeightBand">
|
||||
select band_id as bandId, product_id as productId, version_no as versionNo, section_name as sectionName, country_name as countryName,
|
||||
select band_id as bandId, product_id as productId, term_id as termId, version_no as versionNo, section_name as sectionName, country_name as countryName,
|
||||
country_code as countryCode, zone_name as zoneName, weight_label as weightLabel, weight_min as weightMin,
|
||||
weight_max as weightMax, volume_text as volumeText, charge_name as chargeName, price, price_unit as priceUnit, sort_no as sortNo
|
||||
from pay_weight_band
|
||||
@@ -278,8 +354,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
order by product_id, sort_no, band_id
|
||||
</select>
|
||||
|
||||
<select id="selectWeightBandsByTerm" resultType="com.bms.price.domain.PayWeightBand">
|
||||
select band_id as bandId, product_id as productId, term_id as termId, version_no as versionNo, section_name as sectionName, country_name as countryName,
|
||||
country_code as countryCode, zone_name as zoneName, weight_label as weightLabel, weight_min as weightMin,
|
||||
weight_max as weightMax, volume_text as volumeText, charge_name as chargeName, price, price_unit as priceUnit, sort_no as sortNo
|
||||
from pay_weight_band
|
||||
where term_id in
|
||||
<foreach collection="termIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
order by product_id, sort_no, band_id
|
||||
</select>
|
||||
|
||||
<select id="selectSurcharges" resultType="com.bms.price.domain.PaySurcharge">
|
||||
select surcharge_id as surchargeId, product_id as productId, version_no as versionNo, section_name as sectionName, fee_name as feeName,
|
||||
select surcharge_id as surchargeId, product_id as productId, term_id as termId, version_no as versionNo, section_name as sectionName, fee_name as feeName,
|
||||
zone_name as zoneName, charge_text as chargeText, amount, currency, price_unit as priceUnit, fee_desc as feeDesc,
|
||||
period_text as periodText, effective_start as effectiveStart, effective_end as effectiveEnd,
|
||||
charge_timing as chargeTiming, calc_rule as calcRule, calc_param as calcParam,
|
||||
@@ -290,14 +376,47 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
order by product_id, sort_no, surcharge_id
|
||||
</select>
|
||||
|
||||
<select id="selectSurchargesByTerm" resultType="com.bms.price.domain.PaySurcharge">
|
||||
select surcharge_id as surchargeId, product_id as productId, term_id as termId, version_no as versionNo, section_name as sectionName, fee_name as feeName,
|
||||
zone_name as zoneName, charge_text as chargeText, amount, currency, price_unit as priceUnit, fee_desc as feeDesc,
|
||||
period_text as periodText, effective_start as effectiveStart, effective_end as effectiveEnd,
|
||||
charge_timing as chargeTiming, calc_rule as calcRule, calc_param as calcParam,
|
||||
weight_min as weightMin, weight_max as weightMax, postcode_param as postcodeParam, sort_no as sortNo
|
||||
from pay_surcharge
|
||||
where term_id in
|
||||
<foreach collection="termIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
order by product_id, sort_no, surcharge_id
|
||||
</select>
|
||||
|
||||
<select id="selectExtraLines" resultType="com.bms.price.domain.PayExtraLine">
|
||||
select line_id as lineId, product_id as productId, line_type as lineType, line_text as lineText, sort_no as sortNo
|
||||
select line_id as lineId, product_id as productId, term_id as termId, line_type as lineType, line_text as lineText, sort_no as sortNo
|
||||
from pay_extra_line
|
||||
where product_id in
|
||||
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
order by product_id, sort_no, line_id
|
||||
</select>
|
||||
|
||||
<select id="selectExtraLinesByTerm" resultType="com.bms.price.domain.PayExtraLine">
|
||||
select line_id as lineId, product_id as productId, term_id as termId, line_type as lineType, line_text as lineText, sort_no as sortNo
|
||||
from pay_extra_line
|
||||
where term_id in
|
||||
<foreach collection="termIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
order by product_id, sort_no, line_id
|
||||
</select>
|
||||
|
||||
<delete id="deleteWeightBandsByTerm">
|
||||
delete from pay_weight_band where term_id in
|
||||
<foreach collection="termIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</delete>
|
||||
<delete id="deleteSurchargesByTerm">
|
||||
delete from pay_surcharge where term_id in
|
||||
<foreach collection="termIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</delete>
|
||||
<delete id="deleteExtraLinesByTerm">
|
||||
delete from pay_extra_line where term_id in
|
||||
<foreach collection="termIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</delete>
|
||||
|
||||
<delete id="deleteWeightBands">
|
||||
delete from pay_weight_band where product_id in
|
||||
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
@@ -323,13 +442,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where product_id = #{rootId} or origin_id = #{rootId}
|
||||
</select>
|
||||
|
||||
<!-- 取价只看有效期。生效/失效开关不参与,和历史页说明一致。 -->
|
||||
<!-- 取价只看有效期。生效/失效开关不参与,和历史页说明一致。仅应付公共 term。 -->
|
||||
<select id="selectEffectiveVersionId" resultType="long">
|
||||
select v.product_id
|
||||
from pay_product v
|
||||
inner join pay_product_term t on t.product_id = v.product_id
|
||||
where (v.channel_id is null or v.channel_id = '')
|
||||
and (v.product_id = #{stableId} or v.origin_id = #{stableId})
|
||||
and t.price_set_id is null
|
||||
and t.status = '1'
|
||||
and (t.effective_start is null or t.effective_start <= date(#{bizDate}))
|
||||
and (t.effective_end is null or t.effective_end >= date(#{bizDate}))
|
||||
order by ifnull(v.version_no, 1) desc, v.product_id desc
|
||||
|
||||
@@ -5,7 +5,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<mapper namespace="com.bms.price.mapper.PayProductTermMapper">
|
||||
|
||||
<select id="selectByDict" resultType="com.bms.price.domain.PayProductTerm">
|
||||
select t.term_id as termId, t.dict_value as dictValue, t.product_id as productId, t.version_no as versionNo,
|
||||
select t.term_id as termId, t.dict_value as dictValue, t.product_id as productId, t.price_set_id as priceSetId,
|
||||
t.version_no as versionNo,
|
||||
t.effective_start as effectiveStart, t.effective_end as effectiveEnd, t.status,
|
||||
t.created_by as createBy, t.create_time as createTime, t.remark,
|
||||
p.product_name as productName, p.source_file as sourceFile,
|
||||
@@ -15,29 +16,119 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
join pay_product p on p.product_id = t.product_id
|
||||
where t.dict_value = #{dictValue}
|
||||
and (p.channel_id is null or p.channel_id = '')
|
||||
and t.price_set_id is null
|
||||
order by t.version_no desc, t.term_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectById" resultType="com.bms.price.domain.PayProductTerm">
|
||||
select term_id as termId, dict_value as dictValue, product_id as productId, version_no as versionNo,
|
||||
select term_id as termId, dict_value as dictValue, product_id as productId, price_set_id as priceSetId,
|
||||
version_no as versionNo,
|
||||
effective_start as effectiveStart, effective_end as effectiveEnd, status
|
||||
from pay_product_term
|
||||
where term_id = #{termId}
|
||||
</select>
|
||||
|
||||
<!-- 应付侧当前版:只取公共 term;优先生效(1),其次待生效(0) -->
|
||||
<select id="selectByProductId" resultType="com.bms.price.domain.PayProductTerm">
|
||||
select term_id as termId, dict_value as dictValue, product_id as productId, version_no as versionNo,
|
||||
select term_id as termId, dict_value as dictValue, product_id as productId, price_set_id as priceSetId,
|
||||
version_no as versionNo,
|
||||
effective_start as effectiveStart, effective_end as effectiveEnd, status
|
||||
from pay_product_term
|
||||
where product_id = #{productId}
|
||||
and price_set_id is null
|
||||
and status in ('0', '1')
|
||||
order by case when status = '1' then 0 when status = '0' then 1 else 2 end, version_no desc, term_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!-- 打单套餐产品列表:待生效+生效 -->
|
||||
<select id="selectByPriceSetId" resultType="com.bms.price.domain.PayProductTerm">
|
||||
select t.term_id as termId, t.dict_value as dictValue, t.product_id as productId, t.price_set_id as priceSetId,
|
||||
t.version_no as versionNo,
|
||||
t.effective_start as effectiveStart, t.effective_end as effectiveEnd, t.status,
|
||||
p.product_name as productName, p.source_file as sourceFile,
|
||||
ifnull(t.file_path, p.file_path) as filePath, p.weight_count as weightCount,
|
||||
p.surcharge_count as surchargeCount, p.currency
|
||||
from pay_product_term t
|
||||
join pay_product p on p.product_id = t.product_id
|
||||
where t.price_set_id = #{priceSetId}
|
||||
and t.status in ('0', '1')
|
||||
order by p.product_name, t.term_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectByPriceSetIdAndVersion" resultType="com.bms.price.domain.PayProductTerm">
|
||||
select t.term_id as termId, t.dict_value as dictValue, t.product_id as productId, t.price_set_id as priceSetId,
|
||||
t.version_no as versionNo,
|
||||
t.effective_start as effectiveStart, t.effective_end as effectiveEnd, t.status,
|
||||
p.product_name as productName, p.source_file as sourceFile,
|
||||
ifnull(t.file_path, p.file_path) as filePath, p.weight_count as weightCount,
|
||||
p.surcharge_count as surchargeCount, p.currency
|
||||
from pay_product_term t
|
||||
join pay_product p on p.product_id = t.product_id
|
||||
where t.price_set_id = #{priceSetId}
|
||||
and t.version_no = #{versionNo}
|
||||
order by case when t.status = '1' then 0 when t.status = '0' then 1 else 2 end, p.product_name, t.term_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectTermIdBySetProductAt" resultType="long">
|
||||
select t.term_id
|
||||
from pay_product_term t
|
||||
where t.price_set_id = #{priceSetId}
|
||||
and t.product_id = #{productId}
|
||||
and t.effective_start is not null
|
||||
and t.effective_start <= date(#{bizDate})
|
||||
and (t.effective_end is null or t.effective_end >= date(#{bizDate}))
|
||||
order by ifnull(t.version_no, 1) desc, t.term_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectLabelVersionGroups" resultType="com.bms.price.domain.LabelPackageVersion">
|
||||
select #{priceSetId} as priceSetId,
|
||||
t.version_no as versionNo,
|
||||
min(t.effective_start) as effectiveTime,
|
||||
max(t.effective_end) as expireTime,
|
||||
count(distinct t.product_id) as productCount,
|
||||
sum(case when t.status = '1' then 1 else 0 end) as activeCount,
|
||||
sum(case when t.status = '0' then 1 else 0 end) as pendingCount,
|
||||
min(t.create_time) as createTime
|
||||
from pay_product_term t
|
||||
where t.price_set_id = #{priceSetId}
|
||||
group by t.version_no
|
||||
order by t.version_no desc
|
||||
</select>
|
||||
|
||||
<!-- 启动:写入生效日;未到日 status=0,已到/当天 status=1 -->
|
||||
<update id="updateLabelVersionPeriod">
|
||||
update pay_product_term
|
||||
set effective_start = #{effectiveStart},
|
||||
effective_end = #{effectiveEnd},
|
||||
status = #{status},
|
||||
updated_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where price_set_id = #{priceSetId}
|
||||
and version_no = #{versionNo}
|
||||
and status in ('0', '1')
|
||||
</update>
|
||||
|
||||
<select id="selectMaxVersionByProduct" resultType="int">
|
||||
select ifnull(max(version_no), 0) from pay_product_term where product_id = #{productId}
|
||||
<if test="priceSetId != null">
|
||||
and price_set_id = #{priceSetId}
|
||||
</if>
|
||||
<if test="priceSetId == null">
|
||||
and price_set_id is null
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="selectMaxVersionByPriceSet" resultType="int">
|
||||
select ifnull(max(version_no), 0) from pay_product_term where price_set_id = #{priceSetId}
|
||||
</select>
|
||||
|
||||
<insert id="insertTerm" parameterType="com.bms.price.domain.PayProductTerm" useGeneratedKeys="true" keyProperty="termId">
|
||||
insert into pay_product_term (
|
||||
dict_value, product_id, version_no, effective_start, effective_end, file_path, status, created_by, create_time, remark
|
||||
dict_value, product_id, price_set_id, version_no, effective_start, effective_end, file_path, status, created_by, create_time, remark
|
||||
) values (
|
||||
#{dictValue}, #{productId}, ifnull(#{versionNo}, 1), #{effectiveStart}, #{effectiveEnd}, #{filePath},
|
||||
#{dictValue}, #{productId}, #{priceSetId}, ifnull(#{versionNo}, 1), #{effectiveStart}, #{effectiveEnd}, #{filePath},
|
||||
ifnull(#{status}, '0'), #{createBy}, sysdate(), #{remark}
|
||||
)
|
||||
</insert>
|
||||
@@ -59,9 +150,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where term_id = #{termId}
|
||||
</update>
|
||||
|
||||
<!-- 截断旧版:生效/待生效 → 已失效(2) -->
|
||||
<update id="closeOpenTerms">
|
||||
update pay_product_term
|
||||
set status = '1',
|
||||
set status = '2',
|
||||
effective_end = case
|
||||
when effective_end is not null and effective_end < #{endDate} then effective_end
|
||||
when effective_start is not null and effective_start > #{endDate} then effective_start
|
||||
@@ -70,14 +162,90 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
updated_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where dict_value = #{dictValue}
|
||||
and status in ('0', '1')
|
||||
and price_set_id is null
|
||||
</update>
|
||||
|
||||
<update id="closeOpenTermsBySetProduct">
|
||||
update pay_product_term
|
||||
set status = '2',
|
||||
effective_end = case
|
||||
when effective_end is not null and effective_end < #{endDate} then effective_end
|
||||
when effective_start is not null and effective_start > #{endDate} then effective_start
|
||||
else #{endDate}
|
||||
end,
|
||||
updated_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where product_id = #{productId}
|
||||
and price_set_id = #{priceSetId}
|
||||
and status in ('0', '1')
|
||||
</update>
|
||||
|
||||
<update id="closePendingBySetProduct">
|
||||
update pay_product_term
|
||||
set status = '2',
|
||||
effective_end = case
|
||||
when effective_end is not null and effective_end < #{endDate} then effective_end
|
||||
when effective_start is not null and effective_start > #{endDate} then effective_start
|
||||
else #{endDate}
|
||||
end,
|
||||
updated_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where product_id = #{productId}
|
||||
and price_set_id = #{priceSetId}
|
||||
and status = '0'
|
||||
</update>
|
||||
|
||||
<update id="closeEffectExceptVersion">
|
||||
update pay_product_term
|
||||
set status = '2',
|
||||
effective_end = case
|
||||
when effective_end is not null and effective_end < #{endDate} then effective_end
|
||||
when effective_start is not null and effective_start > #{endDate} then effective_start
|
||||
else #{endDate}
|
||||
end,
|
||||
updated_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where price_set_id = #{priceSetId}
|
||||
and status = '1'
|
||||
and version_no <> #{versionNo}
|
||||
</update>
|
||||
|
||||
<update id="scheduleEndExceptVersion">
|
||||
update pay_product_term
|
||||
set status = '1',
|
||||
effective_end = #{endDate},
|
||||
updated_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where price_set_id = #{priceSetId}
|
||||
and version_no <> #{versionNo}
|
||||
and (
|
||||
status = '1'
|
||||
or (status = '2' and effective_end is not null and effective_end >= #{today})
|
||||
)
|
||||
</update>
|
||||
|
||||
<update id="restorePrematureClosed">
|
||||
update pay_product_term
|
||||
set status = '1',
|
||||
updated_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where price_set_id = #{priceSetId}
|
||||
and status = '2'
|
||||
and effective_end is not null
|
||||
and effective_end >= #{today}
|
||||
</update>
|
||||
|
||||
<delete id="deleteByProductIds">
|
||||
delete from pay_product_term where product_id in
|
||||
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByTermIds">
|
||||
delete from pay_product_term where term_id in
|
||||
<foreach collection="termIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</delete>
|
||||
|
||||
<update id="updateProductEffectiveDate">
|
||||
update pay_product
|
||||
set effective_date = #{effectiveDate},
|
||||
|
||||
@@ -31,11 +31,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
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,
|
||||
case
|
||||
when ifnull(s.set_category, 'PACKAGE') = 'LABEL' then
|
||||
(select count(distinct t.product_id)
|
||||
from pay_product_term t
|
||||
where t.price_set_id = s.price_set_id
|
||||
and t.status = '0')
|
||||
else
|
||||
(select count(distinct p.product_code)
|
||||
from prc_price_set_product p
|
||||
inner join prc_price_set_version v on v.psv_id = p.psv_id
|
||||
where v.price_set_id = s.price_set_id
|
||||
and ifnull(p.is_del, '0') = '0') as product_count,
|
||||
and ifnull(p.is_del, '0') = '0')
|
||||
end as product_count,
|
||||
(select count(1) from prc_customer_price_set c where c.price_set_id = s.price_set_id) as customer_count,
|
||||
(select group_concat(
|
||||
concat(ifnull(c.customer_code, ''),
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
-- 打单套餐一键脚本(可重复执行)
|
||||
-- 合并自:pay_product_term.sql + label_package.sql + label_package_create_menu.sql
|
||||
-- 含:应付有效期表、打单表结构变更、打单/应付相关菜单权限
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ========== 一、pay_product_term 建表 + 存量回填 ==========
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_product_term` (
|
||||
`term_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '有效期ID',
|
||||
`dict_value` varchar(100) NOT NULL COMMENT '产品字典值,同一产品的各版本归在一起',
|
||||
`product_id` bigint(20) NOT NULL COMMENT '这一版价格',
|
||||
`version_no` int(11) NOT NULL DEFAULT 1 COMMENT '版本号',
|
||||
`effective_start` date DEFAULT NULL COMMENT '生效开始',
|
||||
`effective_end` date DEFAULT NULL COMMENT '生效结束,空表示不限',
|
||||
`file_path` varchar(500) DEFAULT NULL COMMENT '这一版源文件OSS地址,oss://bucket/key',
|
||||
`status` char(1) NOT NULL DEFAULT '0' COMMENT '0待生效 1生效 2已失效',
|
||||
`created_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_by` varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (`term_id`),
|
||||
UNIQUE KEY `uk_pay_term_product` (`product_id`),
|
||||
KEY `idx_pay_term_dict` (`dict_value`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应付产品价格有效期';
|
||||
|
||||
-- 已有模板补一条生效记录,不覆盖后来手工改过的有效期
|
||||
INSERT INTO pay_product_term (
|
||||
dict_value, product_id, version_no, effective_start, status, created_by, create_time
|
||||
)
|
||||
SELECT ifnull(nullif(dict_value, ''), product_name), product_id, ifnull(version_no, 1), effective_date, '0',
|
||||
ifnull(created_by, ''), ifnull(create_time, sysdate())
|
||||
FROM pay_product p
|
||||
WHERE (p.channel_id IS NULL OR p.channel_id = '')
|
||||
AND NOT EXISTS (SELECT 1 FROM pay_product_term t WHERE t.product_id = p.product_id);
|
||||
|
||||
-- ========== 二、打单相关表结构变更 ==========
|
||||
|
||||
-- 0) pay_product_term.status:旧 0生效/1失效 → 新 0待生效/1生效/2已失效
|
||||
-- 仅当列注释仍是旧语义时迁移(可重复执行)
|
||||
SET @old_status_comment := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pay_product_term'
|
||||
AND COLUMN_NAME = 'status'
|
||||
AND COLUMN_COMMENT LIKE '%0生效%1失效%'
|
||||
);
|
||||
SET @sql := IF(@old_status_comment > 0,
|
||||
'UPDATE pay_product_term SET status = ''2'' WHERE status = ''1''',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @sql := IF(@old_status_comment > 0,
|
||||
'UPDATE pay_product_term SET status = ''1'' WHERE status = ''0''',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
ALTER TABLE pay_product_term
|
||||
MODIFY COLUMN status char(1) NOT NULL DEFAULT '0' COMMENT '0待生效 1生效 2已失效';
|
||||
|
||||
-- 1) prc_price_set.set_category 注释增加 LABEL
|
||||
ALTER TABLE prc_price_set
|
||||
MODIFY COLUMN set_category varchar(20) NOT NULL DEFAULT 'PACKAGE'
|
||||
COMMENT '分类 PACKAGE=普通套餐 EXCLUSIVE=专属报价 LABEL=打单套餐';
|
||||
|
||||
-- 2) pay_product_term.price_set_id
|
||||
SET @exists := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pay_product_term'
|
||||
AND COLUMN_NAME = 'price_set_id'
|
||||
);
|
||||
SET @sql := IF(@exists = 0,
|
||||
'ALTER TABLE pay_product_term
|
||||
ADD COLUMN price_set_id bigint(20) DEFAULT NULL
|
||||
COMMENT ''打单套餐ID,对应 prc_price_set.price_set_id;应付公共模板为空''
|
||||
AFTER product_id,
|
||||
ADD KEY idx_pay_term_price_set (price_set_id)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 3) 去掉一产品一条 term 的唯一约束,支持一产品多周期
|
||||
SET @uk := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pay_product_term'
|
||||
AND INDEX_NAME = 'uk_pay_term_product'
|
||||
);
|
||||
SET @sql := IF(@uk > 0, 'ALTER TABLE pay_product_term DROP INDEX uk_pay_term_product', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @idx1 := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pay_product_term'
|
||||
AND INDEX_NAME = 'idx_pay_term_set_product_time'
|
||||
);
|
||||
SET @sql := IF(@idx1 = 0,
|
||||
'ALTER TABLE pay_product_term ADD KEY idx_pay_term_set_product_time (price_set_id, product_id, effective_start, effective_end)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @idx2 := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pay_product_term'
|
||||
AND INDEX_NAME = 'idx_pay_term_product_time'
|
||||
);
|
||||
SET @sql := IF(@idx2 = 0,
|
||||
'ALTER TABLE pay_product_term ADD KEY idx_pay_term_product_time (product_id, effective_start, effective_end)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 4) 明细表增加 term_id
|
||||
SET @exists := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_weight_band' AND COLUMN_NAME = 'term_id'
|
||||
);
|
||||
SET @sql := IF(@exists = 0,
|
||||
'ALTER TABLE pay_weight_band
|
||||
ADD COLUMN term_id bigint(20) DEFAULT NULL
|
||||
COMMENT ''价卡版本周期ID,对应 pay_product_term.term_id;与 product_id 组成明细唯一维度''
|
||||
AFTER product_id,
|
||||
ADD KEY idx_pay_weight_product_term (product_id, term_id)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @exists := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_surcharge' AND COLUMN_NAME = 'term_id'
|
||||
);
|
||||
SET @sql := IF(@exists = 0,
|
||||
'ALTER TABLE pay_surcharge
|
||||
ADD COLUMN term_id bigint(20) DEFAULT NULL
|
||||
COMMENT ''价卡版本周期ID,对应 pay_product_term.term_id;与 product_id 组成明细唯一维度''
|
||||
AFTER product_id,
|
||||
ADD KEY idx_pay_surcharge_product_term (product_id, term_id)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @exists := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_extra_line' AND COLUMN_NAME = 'term_id'
|
||||
);
|
||||
SET @sql := IF(@exists = 0,
|
||||
'ALTER TABLE pay_extra_line
|
||||
ADD COLUMN term_id bigint(20) DEFAULT NULL
|
||||
COMMENT ''价卡版本周期ID,对应 pay_product_term.term_id;与 product_id 组成明细唯一维度''
|
||||
AFTER product_id,
|
||||
ADD KEY idx_pay_extra_product_term (product_id, term_id)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 5) 存量明细回填 term_id(一产品多 term 时取最新一条)
|
||||
UPDATE pay_weight_band b
|
||||
INNER JOIN (
|
||||
SELECT t1.product_id, t1.term_id
|
||||
FROM pay_product_term t1
|
||||
INNER JOIN (
|
||||
SELECT product_id, MAX(term_id) AS term_id
|
||||
FROM pay_product_term
|
||||
GROUP BY product_id
|
||||
) t2 ON t2.product_id = t1.product_id AND t2.term_id = t1.term_id
|
||||
) t ON t.product_id = b.product_id
|
||||
SET b.term_id = t.term_id
|
||||
WHERE b.term_id IS NULL;
|
||||
|
||||
UPDATE pay_surcharge s
|
||||
INNER JOIN (
|
||||
SELECT t1.product_id, t1.term_id
|
||||
FROM pay_product_term t1
|
||||
INNER JOIN (
|
||||
SELECT product_id, MAX(term_id) AS term_id
|
||||
FROM pay_product_term
|
||||
GROUP BY product_id
|
||||
) t2 ON t2.product_id = t1.product_id AND t2.term_id = t1.term_id
|
||||
) t ON t.product_id = s.product_id
|
||||
SET s.term_id = t.term_id
|
||||
WHERE s.term_id IS NULL;
|
||||
|
||||
UPDATE pay_extra_line e
|
||||
INNER JOIN (
|
||||
SELECT t1.product_id, t1.term_id
|
||||
FROM pay_product_term t1
|
||||
INNER JOIN (
|
||||
SELECT product_id, MAX(term_id) AS term_id
|
||||
FROM pay_product_term
|
||||
GROUP BY product_id
|
||||
) t2 ON t2.product_id = t1.product_id AND t2.term_id = t1.term_id
|
||||
) t ON t.product_id = e.product_id
|
||||
SET e.term_id = t.term_id
|
||||
WHERE e.term_id IS NULL;
|
||||
|
||||
-- 6) 删除渠道映射上的套餐字段
|
||||
SET @exists := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'srv_channel_product_map'
|
||||
AND COLUMN_NAME = 'price_set_id'
|
||||
);
|
||||
SET @sql := IF(@exists > 0,
|
||||
'ALTER TABLE srv_channel_product_map DROP INDEX idx_channel_product_map_price_set, DROP COLUMN price_set_id',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ========== 三、应付配置菜单(price:pay:edit) ==========
|
||||
|
||||
SET @menuId := (SELECT menu_id FROM sys_menu WHERE perms = 'price:pay:list' LIMIT 1);
|
||||
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||
SELECT '应付配置修改', @menuId, 5, '', '', 1, 0, 'F', '0', '0', 'price:pay:edit', '#', 'admin', sysdate()
|
||||
WHERE @menuId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'price:pay:edit');
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT DISTINCT rm.role_id, m.menu_id
|
||||
FROM sys_role_menu rm
|
||||
JOIN sys_menu src ON src.menu_id = rm.menu_id AND src.perms = 'price:pay:list'
|
||||
JOIN sys_menu m ON m.perms = 'price:pay:edit'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = m.menu_id
|
||||
);
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, m.menu_id
|
||||
FROM sys_menu m
|
||||
WHERE m.perms = 'price:pay:edit'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu x WHERE x.role_id = 1 AND x.menu_id = m.menu_id
|
||||
);
|
||||
|
||||
-- ========== 四、打单套餐菜单权限(挂在套餐管理下) ==========
|
||||
|
||||
SET @pkgParentId := (
|
||||
SELECT menu_id FROM sys_menu
|
||||
WHERE menu_id = 2200
|
||||
OR (menu_name = N'套餐管理' AND parent_id = 0 AND menu_type = 'M')
|
||||
ORDER BY CASE WHEN menu_id = 2200 THEN 0 ELSE 1 END, menu_id
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
-- 列表
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2620, N'打单套餐', @pkgParentId, 3, 'label', 'price/label/labelPackage', '', 'LabelPackage', 1, 0, 'C', '0', '0', 'price:label:list', 'list', 'admin', sysdate(), N'打单套餐列表'
|
||||
FROM DUAL WHERE @pkgParentId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2620 OR perms = 'price:label:list');
|
||||
|
||||
UPDATE sys_menu
|
||||
SET menu_name = N'打单套餐',
|
||||
parent_id = @pkgParentId,
|
||||
order_num = 3,
|
||||
path = 'label',
|
||||
component = 'price/label/labelPackage',
|
||||
route_name = 'LabelPackage',
|
||||
perms = 'price:label:list',
|
||||
remark = N'打单套餐列表',
|
||||
update_by = 'admin',
|
||||
update_time = sysdate()
|
||||
WHERE @pkgParentId IS NOT NULL AND (menu_id = 2620 OR perms = 'price:label:list');
|
||||
|
||||
-- 隐藏新建页
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2628, N'打单套餐新建', @pkgParentId, 7, 'labelCreate', 'price/label/create', '', 'LabelPackageCreate', 1, 0, 'C', '1', '0', 'price:label:add', '#', 'admin', sysdate(), N'打单套餐新建(基本信息+Excel导入)'
|
||||
FROM DUAL WHERE @pkgParentId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2628 OR path = 'labelCreate');
|
||||
|
||||
UPDATE sys_menu
|
||||
SET menu_name = N'打单套餐新建',
|
||||
parent_id = @pkgParentId,
|
||||
order_num = 7,
|
||||
path = 'labelCreate',
|
||||
component = 'price/label/create',
|
||||
route_name = 'LabelPackageCreate',
|
||||
perms = 'price:label:add',
|
||||
visible = '1',
|
||||
remark = N'打单套餐新建(基本信息+Excel导入)',
|
||||
update_by = 'admin',
|
||||
update_time = sysdate()
|
||||
WHERE @pkgParentId IS NOT NULL AND (menu_id = 2628 OR path = 'labelCreate');
|
||||
|
||||
-- 隐藏详情页
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2627, N'打单套餐详情', @pkgParentId, 8, 'labelDetail', 'price/label/detail', '', 'LabelPackageDetail', 1, 0, 'C', '1', '0', 'price:label:query', '#', 'admin', sysdate(), N'打单套餐产品明细'
|
||||
FROM DUAL WHERE @pkgParentId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2627 OR path = 'labelDetail');
|
||||
|
||||
UPDATE sys_menu
|
||||
SET menu_name = N'打单套餐详情',
|
||||
parent_id = @pkgParentId,
|
||||
order_num = 8,
|
||||
path = 'labelDetail',
|
||||
component = 'price/label/detail',
|
||||
route_name = 'LabelPackageDetail',
|
||||
perms = 'price:label:query',
|
||||
visible = '1',
|
||||
remark = N'打单套餐产品明细',
|
||||
update_by = 'admin',
|
||||
update_time = sysdate()
|
||||
WHERE @pkgParentId IS NOT NULL AND (menu_id = 2627 OR path = 'labelDetail');
|
||||
|
||||
-- 按钮
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2621, N'查询', 2620, 1, '', NULL, 1, 0, 'F', '0', '0', 'price:label:query', '#', 'admin', sysdate(), N''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2621 OR (parent_id = 2620 AND perms = 'price:label:query' AND menu_type = 'F'));
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2622, N'新增', 2620, 2, '', NULL, 1, 0, 'F', '0', '0', 'price:label:add', '#', 'admin', sysdate(), N''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2622 OR (parent_id = 2620 AND perms = 'price:label:add' AND menu_type = 'F'));
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2623, N'修改', 2620, 3, '', NULL, 1, 0, 'F', '0', '0', 'price:label:edit', '#', 'admin', sysdate(), N''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2623 OR (parent_id = 2620 AND perms = 'price:label:edit' AND menu_type = 'F'));
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2624, N'删除', 2620, 4, '', NULL, 1, 0, 'F', '0', '0', 'price:label:remove', '#', 'admin', sysdate(), N''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2624 OR (parent_id = 2620 AND perms = 'price:label:remove' AND menu_type = 'F'));
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2625, N'导入', 2620, 5, '', NULL, 1, 0, 'F', '0', '0', 'price:label:import', '#', 'admin', sysdate(), N''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2625 OR (parent_id = 2620 AND perms = 'price:label:import' AND menu_type = 'F'));
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2626, N'启停', 2620, 6, '', NULL, 1, 0, 'F', '0', '0', 'price:label:audit', '#', 'admin', sysdate(), N''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2626 OR (parent_id = 2620 AND perms = 'price:label:audit' AND menu_type = 'F'));
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, m.menu_id FROM (
|
||||
SELECT 2620 AS menu_id UNION SELECT 2621 UNION SELECT 2622
|
||||
UNION SELECT 2623 UNION SELECT 2624 UNION SELECT 2625 UNION SELECT 2626
|
||||
UNION SELECT 2627 UNION SELECT 2628
|
||||
) m
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
|
||||
|
||||
-- ========== 五、打单计费热路径索引(可重复执行) ==========
|
||||
-- 命中顺序:客户绑套餐 → 渠道 AR 映产品 → term 按套餐+产品+日期 → 明细按 term_id
|
||||
|
||||
-- 5.1 客户按 customer_code 取绑定(selectEffectiveByCustomerCode)
|
||||
SET @idx := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'prc_customer_price_set'
|
||||
AND INDEX_NAME = 'idx_cps_customer_code'
|
||||
);
|
||||
SET @sql := IF(@idx = 0,
|
||||
'ALTER TABLE prc_customer_price_set ADD KEY idx_cps_customer_code (customer_code)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 5.2 套餐壳按分类+状态(列表/启停过滤;计费按 PK 取,顺带加速 LABEL 筛)
|
||||
SET @idx := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'prc_price_set'
|
||||
AND INDEX_NAME = 'idx_ps_category_status'
|
||||
);
|
||||
SET @sql := IF(@idx = 0,
|
||||
'ALTER TABLE prc_price_set ADD KEY idx_ps_category_status (set_category, status)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 5.3 渠道 AR 映射:uk_channel_pay_type(channel_id, pay_type) 已存在,补 product_id 侧已有;确保 channel_code 索引
|
||||
SET @idx := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'srv_channel_product_map'
|
||||
AND INDEX_NAME = 'idx_channel_product_map_channel_code'
|
||||
);
|
||||
SET @sql := IF(@idx = 0,
|
||||
'ALTER TABLE srv_channel_product_map ADD KEY idx_channel_product_map_channel_code (channel_code)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 5.4 term 计费命中:套餐+产品+生效窗(selectTermIdBySetProductAt)— 已有则跳过
|
||||
SET @idx := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pay_product_term'
|
||||
AND INDEX_NAME = 'idx_pay_term_set_product_time'
|
||||
);
|
||||
SET @sql := IF(@idx = 0,
|
||||
'ALTER TABLE pay_product_term ADD KEY idx_pay_term_set_product_time (price_set_id, product_id, effective_start, effective_end)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 5.5 term 按产品查是否挂套餐(selectLabelProductIdByDict 的 exists)
|
||||
SET @idx := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pay_product_term'
|
||||
AND INDEX_NAME = 'idx_pay_term_product_set'
|
||||
);
|
||||
SET @sql := IF(@idx = 0,
|
||||
'ALTER TABLE pay_product_term ADD KEY idx_pay_term_product_set (product_id, price_set_id)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 5.6 打单产品字典命中(dict_value + AR)
|
||||
SET @idx := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pay_product'
|
||||
AND INDEX_NAME = 'idx_pay_product_dict_type'
|
||||
);
|
||||
SET @sql := IF(@idx = 0,
|
||||
'ALTER TABLE pay_product ADD KEY idx_pay_product_dict_type (dict_value, product_type)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 5.7 明细按 term_id 装载(select*ByTerm);原索引以 product_id 领先,补 term 单列
|
||||
SET @idx := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pay_weight_band'
|
||||
AND INDEX_NAME = 'idx_pay_weight_term'
|
||||
);
|
||||
SET @sql := IF(@idx = 0,
|
||||
'ALTER TABLE pay_weight_band ADD KEY idx_pay_weight_term (term_id)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @idx := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pay_surcharge'
|
||||
AND INDEX_NAME = 'idx_pay_surcharge_term'
|
||||
);
|
||||
SET @sql := IF(@idx = 0,
|
||||
'ALTER TABLE pay_surcharge ADD KEY idx_pay_surcharge_term (term_id)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @idx := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pay_extra_line'
|
||||
AND INDEX_NAME = 'idx_pay_extra_term'
|
||||
);
|
||||
SET @sql := IF(@idx = 0,
|
||||
'ALTER TABLE pay_extra_line ADD KEY idx_pay_extra_term (term_id)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 5.8 订单扩展按渠道反查(可选批量重计时);bill_id / order_no 已有唯一键
|
||||
SET @idx := (
|
||||
SELECT COUNT(1) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'prc_order_bill_ext'
|
||||
AND INDEX_NAME = 'idx_order_bill_ext_channel'
|
||||
);
|
||||
SET @sql := IF(@idx = 0,
|
||||
'ALTER TABLE prc_order_bill_ext ADD KEY idx_order_bill_ext_channel (product_channel_id)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@@ -1,54 +0,0 @@
|
||||
-- 应付产品有效期。同一产品多次导入各留一版,用这张表记录生效区间和是否生效。可重复执行。
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_product_term` (
|
||||
`term_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '有效期ID',
|
||||
`dict_value` varchar(100) NOT NULL COMMENT '产品字典值,同一产品的各版本归在一起',
|
||||
`product_id` bigint(20) NOT NULL COMMENT '这一版价格',
|
||||
`version_no` int(11) NOT NULL DEFAULT 1 COMMENT '版本号',
|
||||
`effective_start` date DEFAULT NULL COMMENT '生效开始',
|
||||
`effective_end` date DEFAULT NULL COMMENT '生效结束,空表示不限',
|
||||
`file_path` varchar(500) DEFAULT NULL COMMENT '这一版源文件OSS地址,oss://bucket/key',
|
||||
`status` char(1) NOT NULL DEFAULT '0' COMMENT '0生效 1失效',
|
||||
`created_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_by` varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (`term_id`),
|
||||
UNIQUE KEY `uk_pay_term_product` (`product_id`),
|
||||
KEY `idx_pay_term_dict` (`dict_value`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应付产品价格有效期';
|
||||
|
||||
-- 已有模板补一条生效记录,不覆盖后来手工改过的有效期
|
||||
INSERT INTO pay_product_term (
|
||||
dict_value, product_id, version_no, effective_start, status, created_by, create_time
|
||||
)
|
||||
SELECT ifnull(nullif(dict_value, ''), product_name), product_id, ifnull(version_no, 1), effective_date, '0',
|
||||
ifnull(created_by, ''), ifnull(create_time, sysdate())
|
||||
FROM pay_product p
|
||||
WHERE (p.channel_id IS NULL OR p.channel_id = '')
|
||||
AND NOT EXISTS (SELECT 1 FROM pay_product_term t WHERE t.product_id = p.product_id);
|
||||
|
||||
SET @menuId := (SELECT menu_id FROM sys_menu WHERE perms = 'price:pay:list' LIMIT 1);
|
||||
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||
SELECT '应付配置修改', @menuId, 5, '', '', 1, 0, 'F', '0', '0', 'price:pay:edit', '#', 'admin', sysdate()
|
||||
WHERE @menuId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'price:pay:edit');
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT DISTINCT rm.role_id, m.menu_id
|
||||
FROM sys_role_menu rm
|
||||
JOIN sys_menu src ON src.menu_id = rm.menu_id AND src.perms = 'price:pay:list'
|
||||
JOIN sys_menu m ON m.perms = 'price:pay:edit'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = m.menu_id
|
||||
);
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, m.menu_id
|
||||
FROM sys_menu m
|
||||
WHERE m.perms = 'price:pay:edit'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu x WHERE x.role_id = 1 AND x.menu_id = m.menu_id
|
||||
);
|
||||
@@ -0,0 +1,156 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listLabelPackage(query) {
|
||||
return request({
|
||||
url: '/price/label/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function getLabelPackage(priceSetId) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function updateLabelBasic(data) {
|
||||
return request({
|
||||
url: '/price/label/basic',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function disableLabelPackage(priceSetId) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/disable',
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function enableLabelPackage(priceSetId) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/enable',
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function delLabelPackage(priceSetIds) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetIds,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function previewLabel(file, dictValue, progressId, onUploadProgress, priceSetId) {
|
||||
const data = new FormData()
|
||||
data.append('file', file)
|
||||
if (dictValue) data.append('dictValue', dictValue)
|
||||
if (progressId) data.append('progressId', progressId)
|
||||
if (priceSetId) data.append('priceSetId', priceSetId)
|
||||
return request({
|
||||
url: '/price/label/preview',
|
||||
method: 'post',
|
||||
data,
|
||||
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
|
||||
timeout: 10 * 60 * 1000,
|
||||
onUploadProgress
|
||||
})
|
||||
}
|
||||
|
||||
export function labelParseProgress(progressId) {
|
||||
return request({
|
||||
url: '/price/label/preview/progress',
|
||||
method: 'get',
|
||||
params: { progressId },
|
||||
headers: { repeatSubmit: false }
|
||||
})
|
||||
}
|
||||
|
||||
export function saveLabelImport(data) {
|
||||
return request({
|
||||
url: '/price/label/importSave',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 10 * 60 * 1000
|
||||
})
|
||||
}
|
||||
|
||||
export function listLabelVersions(priceSetId) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/versions',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function activateLabelVersion(priceSetId, versionNo, data) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/versions/' + versionNo + '/activate',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function listLabelProducts(priceSetId, versionNo) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/products',
|
||||
method: 'get',
|
||||
params: versionNo != null ? { versionNo } : undefined
|
||||
})
|
||||
}
|
||||
|
||||
export function listLabelCustomer(query) {
|
||||
return request({
|
||||
url: '/price/label/customer/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function addLabelCustomer(data) {
|
||||
return request({
|
||||
url: '/price/label/customer',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function batchAddLabelCustomer(data) {
|
||||
return request({
|
||||
url: '/price/label/customer/batch',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateLabelCustomer(data) {
|
||||
return request({
|
||||
url: '/price/label/customer',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function delLabelCustomer(ids) {
|
||||
return request({
|
||||
url: '/price/label/customer/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function getLabelProduct(productId, termId) {
|
||||
return request({
|
||||
url: '/price/label/product/' + productId,
|
||||
method: 'get',
|
||||
params: termId ? { termId } : undefined
|
||||
})
|
||||
}
|
||||
|
||||
export function removeLabelProducts(priceSetId, productIds) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/products/' + productIds,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -40,9 +40,9 @@
|
||||
<template #col-termStatus="{ row }">
|
||||
<el-switch
|
||||
v-if="row.termId"
|
||||
:model-value="row.termStatus === '1' ? '1' : '0'"
|
||||
active-value="0"
|
||||
inactive-value="1"
|
||||
:model-value="row.termStatus === '1' ? '1' : '2'"
|
||||
active-value="1"
|
||||
inactive-value="2"
|
||||
inline-prompt
|
||||
active-text="生效"
|
||||
inactive-text="失效"
|
||||
@@ -87,9 +87,9 @@
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-if="row.termId"
|
||||
:model-value="row.status === '1' ? '1' : '0'"
|
||||
active-value="0"
|
||||
inactive-value="1"
|
||||
:model-value="row.status === '1' ? '1' : '2'"
|
||||
active-value="1"
|
||||
inactive-value="2"
|
||||
inline-prompt
|
||||
active-text="生效"
|
||||
inactive-text="失效"
|
||||
@@ -385,11 +385,12 @@ function saveTerm() {
|
||||
}
|
||||
|
||||
function changeStatus(row, field, status) {
|
||||
if (!row?.termId || (status !== '0' && status !== '1') || row[field] === status) return
|
||||
// pay_product_term.status:0待生效 1生效 2已失效;开关只在 生效/已失效 间切换
|
||||
if (!row?.termId || (status !== '1' && status !== '2') || row[field] === status) return
|
||||
const previous = row[field]
|
||||
row[field] = status
|
||||
changePayTermStatus({ termId: row.termId, productId: row.productId, status }).then(() => {
|
||||
proxy.$modal.msgSuccess(status === '0' ? '已设为生效' : '已设为失效')
|
||||
proxy.$modal.msgSuccess(status === '1' ? '已设为生效' : '已设为失效')
|
||||
tableRef.value.refresh()
|
||||
if (historyVisible.value) loadHistory()
|
||||
}).catch(() => {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="导入打单价卡"
|
||||
:width="autoFillOnParse ? '560px' : '1080px'"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="onClosed"
|
||||
>
|
||||
<label-import-panel
|
||||
ref="panelRef"
|
||||
:auto-fill-on-parse="autoFillOnParse"
|
||||
:price-set-id="priceSetId"
|
||||
@parsed="onParsed"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">{{ autoFillOnParse ? '关闭' : '取消' }}</el-button>
|
||||
<el-button v-if="!autoFillOnParse" type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import LabelImportPanel from './LabelImportPanel.vue'
|
||||
|
||||
const props = defineProps({
|
||||
/** 已有套餐时传入,弹窗内直接落库(详情页) */
|
||||
priceSetId: { type: [Number, String], default: null },
|
||||
/**
|
||||
* 仅创建页传 true:解析成功立刻回填页面,不点确定/保存
|
||||
* 详情页默认 false:预览勾选后点保存落库
|
||||
*/
|
||||
autoFillOnParse: { type: Boolean, default: false }
|
||||
})
|
||||
const emit = defineEmits(['saved', 'confirm'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const visible = ref(false)
|
||||
const saving = ref(false)
|
||||
const panelRef = ref()
|
||||
const autoFilled = ref(false)
|
||||
|
||||
function open() {
|
||||
autoFilled.value = false
|
||||
visible.value = true
|
||||
nextTick(() => panelRef.value?.reset())
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
panelRef.value?.reset()
|
||||
saving.value = false
|
||||
autoFilled.value = false
|
||||
}
|
||||
|
||||
/** 仅创建页(autoFillOnParse)生效 */
|
||||
function onParsed(payload) {
|
||||
if (!props.autoFillOnParse || autoFilled.value) return
|
||||
if (!payload?.products?.length) return
|
||||
autoFilled.value = true
|
||||
emit('confirm', payload)
|
||||
visible.value = false
|
||||
proxy.$modal.msgSuccess(`已导入 ${payload.products.length} 个产品`)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!props.priceSetId) {
|
||||
proxy.$modal.msgError('缺少套餐ID')
|
||||
return
|
||||
}
|
||||
if (!panelRef.value?.validateReady()) return
|
||||
const count = panelRef.value.selectedCount || 0
|
||||
try {
|
||||
await proxy.$modal.confirm(`将保存 ${count} 个应收产品到本套餐。`)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const result = await panelRef.value.submit({ priceSetId: props.priceSetId })
|
||||
visible.value = false
|
||||
ElMessageBox.close()
|
||||
proxy.$modal.msgSuccess(result.message || '保存成功')
|
||||
emit('saved', { priceSetId: result.priceSetId || props.priceSetId })
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -0,0 +1,345 @@
|
||||
<template>
|
||||
<div class="label-import-panel">
|
||||
<el-form label-width="96px" class="import-form">
|
||||
<el-form-item label="支付类型">
|
||||
<el-tag type="success">应收 AR</el-tag>
|
||||
<span class="form-tip">打单套餐固定应收,不显示建价选项,直接 Excel 导入</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品">
|
||||
<el-select v-model="dictValue" clearable filterable placeholder="不选则按工作表名称匹配全部字典产品" style="width: 420px">
|
||||
<el-option v-for="item in payment_product_type" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-upload
|
||||
:key="uploadKey"
|
||||
drag
|
||||
accept=".xlsx"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:disabled="recognizing"
|
||||
@change="onFile"
|
||||
>
|
||||
<el-icon class="upload-icon"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将 xlsx 价卡拖到此处,或<em>点击选择</em></div>
|
||||
<template #tip>
|
||||
<div class="upload-tip">
|
||||
{{ autoFillOnParse
|
||||
? '解析成功后自动回填到创建页产品价卡列表;打单保存与应付独立,弹窗内不展示表格。'
|
||||
: '模板以威狮 B2C 末端派送价卡为准。选一个产品就只读对应工作表。' }}
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<div v-if="recognizing" class="parse-progress">
|
||||
<el-progress :percentage="parsePercent" :stroke-width="16" :status="parseStatus" striped striped-flow :duration="12" />
|
||||
<div class="parse-progress-msg">{{ parseMessage }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建页 autoFillOnParse:不展示弹窗预览表,结果回填页面;详情等入口保留预览勾选 -->
|
||||
<div v-if="preview && !autoFillOnParse" v-loading="recognizing" class="preview-box">
|
||||
<el-alert
|
||||
:title="`${preview.sourceFile || ''} 归属 ${preview.supplierName || '-'} 生效 ${preview.effectiveDate || '-'} 共 ${products.length} 个产品`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<div class="preview-bar">
|
||||
<el-switch v-model="overwrite" active-text="本套餐已存在时另存为新版本" />
|
||||
<span class="preview-hint">仅本打单套餐维度;与应付公共价卡互不影响。开启后同产品编号不变,旧价留在历史 term。</span>
|
||||
</div>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
:data="products"
|
||||
border
|
||||
size="small"
|
||||
max-height="420"
|
||||
row-key="rowKey"
|
||||
@selection-change="onSelect"
|
||||
>
|
||||
<el-table-column type="selection" width="48" :selectable="canSelect" />
|
||||
<el-table-column prop="productName" label="产品" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="sheetName" label="报价工作表" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="countryScope" label="国家/口岸" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="relatedSheet" label="关联分区表" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="weightCount" label="重量段" width="80" align="right" />
|
||||
<el-table-column prop="surchargeCount" label="附加费" width="80" align="right" />
|
||||
<el-table-column label="识别" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.parseStatus)" size="small">{{ statusText(row.parseStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库中" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.duplicated" type="warning" size="small">已存在</el-tag>
|
||||
<span v-else>新记录</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="openDetail(row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div v-if="skippedText" class="upload-tip">未匹配字典、已跳过:{{ skippedText }}</div>
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
|
||||
<pay-config-detail :product="detailProduct" :editable="false" />
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { previewLabel, labelParseProgress, saveLabelImport } from '@/api/price/label'
|
||||
import { useDict } from '@/utils/dict'
|
||||
import PayConfigDetail from '@/views/price/basic/pay/components/PayConfigDetail.vue'
|
||||
|
||||
const props = defineProps({
|
||||
/** 仅创建页开启:解析成功后立刻抛出勾选结果,其它入口保持预览后手动保存 */
|
||||
autoFillOnParse: { type: Boolean, default: false },
|
||||
/** 详情再导入时传入,用于「本套餐已存在」判断;新建不传 */
|
||||
priceSetId: { type: [Number, String], default: null }
|
||||
})
|
||||
const emit = defineEmits(['parsed'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { payment_product_type } = useDict('payment_product_type')
|
||||
|
||||
const recognizing = ref(false)
|
||||
const overwrite = ref(false)
|
||||
const dictValue = ref('')
|
||||
const preview = ref(null)
|
||||
const selected = ref([])
|
||||
const tableRef = ref()
|
||||
const uploadKey = ref(0)
|
||||
const parsePercent = ref(0)
|
||||
const parseMessage = ref('')
|
||||
const parseStatus = ref(undefined)
|
||||
const active = ref(true)
|
||||
const detailVisible = ref(false)
|
||||
const detailProduct = ref(null)
|
||||
|
||||
const products = computed(() => preview.value?.products || [])
|
||||
const skippedText = computed(() => (preview.value?.skippedSheets || []).filter(Boolean).join('、'))
|
||||
const selectedCount = computed(() => selected.value.length)
|
||||
const detailTitle = computed(() => {
|
||||
if (!detailProduct.value) return '产品明细'
|
||||
return detailProduct.value.productName || detailProduct.value.sheetName || '产品明细'
|
||||
})
|
||||
|
||||
let parseSeq = 0
|
||||
let progressTimer = null
|
||||
|
||||
function reset() {
|
||||
parseSeq += 1
|
||||
preview.value = null
|
||||
selected.value = []
|
||||
overwrite.value = false
|
||||
dictValue.value = ''
|
||||
recognizing.value = false
|
||||
parsePercent.value = 0
|
||||
parseMessage.value = ''
|
||||
parseStatus.value = undefined
|
||||
detailVisible.value = false
|
||||
detailProduct.value = null
|
||||
stopProgress()
|
||||
uploadKey.value += 1
|
||||
}
|
||||
|
||||
function openDetail(row) {
|
||||
detailProduct.value = row || null
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
async function onFile(uploadFile) {
|
||||
const raw = uploadFile?.raw
|
||||
if (!raw) return
|
||||
if (!raw.name.toLowerCase().endsWith('.xlsx')) {
|
||||
proxy.$modal.msgError('请上传xlsx价卡')
|
||||
return
|
||||
}
|
||||
const seq = ++parseSeq
|
||||
const progressId = createProgressId()
|
||||
recognizing.value = true
|
||||
parsePercent.value = 0
|
||||
parseMessage.value = '正在上传价卡…'
|
||||
parseStatus.value = undefined
|
||||
startProgress(seq, progressId)
|
||||
try {
|
||||
const res = await previewLabel(raw, dictValue.value, progressId, event => {
|
||||
if (seq !== parseSeq || !event?.total) return
|
||||
const uploaded = Math.min(8, Math.round(event.loaded * 8 / event.total))
|
||||
if (uploaded > parsePercent.value) {
|
||||
parsePercent.value = uploaded
|
||||
parseMessage.value = '正在上传价卡…'
|
||||
}
|
||||
}, props.priceSetId)
|
||||
if (seq !== parseSeq) return
|
||||
parsePercent.value = 100
|
||||
parseMessage.value = '解析完成'
|
||||
parseStatus.value = 'success'
|
||||
const data = res.data || {}
|
||||
data.products = (data.products || []).map((item, index) => ({
|
||||
...item,
|
||||
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${index}`
|
||||
}))
|
||||
preview.value = data
|
||||
const picked = data.products.filter(row => canSelect(row))
|
||||
selected.value = picked
|
||||
// 详情等入口:预览表勾选;创建页不渲染预览表,直接回填页面
|
||||
if (!props.autoFillOnParse) {
|
||||
await nextTick()
|
||||
tableRef.value?.clearSelection()
|
||||
picked.forEach(row => tableRef.value?.toggleRowSelection(row, true))
|
||||
} else if (picked.length) {
|
||||
emit('parsed', getSelectionPayload())
|
||||
} else {
|
||||
proxy.$modal.msgWarning(
|
||||
props.priceSetId
|
||||
? '没有可导入的产品,请调整产品筛选或开启「本套餐已存在时另存为新版本」后重试'
|
||||
: '没有可导入的产品,请调整产品筛选或检查价卡工作表是否匹配字典'
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (seq !== parseSeq) return
|
||||
parseStatus.value = 'exception'
|
||||
parseMessage.value = error?.message || '价卡解析失败'
|
||||
} finally {
|
||||
if (seq === parseSeq) {
|
||||
stopProgress()
|
||||
recognizing.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createProgressId() {
|
||||
if (window.crypto?.randomUUID) return window.crypto.randomUUID()
|
||||
return `lbl${Date.now().toString(16)}${Math.random().toString(16).slice(2, 10)}`
|
||||
}
|
||||
|
||||
function startProgress(seq, progressId) {
|
||||
stopProgress()
|
||||
progressTimer = window.setInterval(async () => {
|
||||
if (seq !== parseSeq) return
|
||||
try {
|
||||
const res = await labelParseProgress(progressId)
|
||||
if (seq !== parseSeq) return
|
||||
const data = res.data || {}
|
||||
if (!data.status || data.status === 'PENDING') return
|
||||
const server = Math.min(99, Number(data.percent) || 0)
|
||||
if (server >= parsePercent.value) {
|
||||
parsePercent.value = server
|
||||
if (data.message) parseMessage.value = data.message
|
||||
}
|
||||
if (data.status === 'FAIL') parseStatus.value = 'exception'
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 400)
|
||||
}
|
||||
|
||||
function stopProgress() {
|
||||
if (progressTimer) {
|
||||
window.clearInterval(progressTimer)
|
||||
progressTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function canSelect(row) {
|
||||
if (row.parseStatus === 'MISSING') return false
|
||||
return overwrite.value || !row.duplicated
|
||||
}
|
||||
|
||||
watch(overwrite, async () => {
|
||||
if (!active.value || !preview.value) return
|
||||
const picked = products.value.filter(row => canSelect(row))
|
||||
selected.value = picked
|
||||
await nextTick()
|
||||
tableRef.value?.clearSelection()
|
||||
picked.forEach(row => tableRef.value?.toggleRowSelection(row, true))
|
||||
})
|
||||
|
||||
function onSelect(rows) {
|
||||
if (!active.value) return
|
||||
selected.value = rows || []
|
||||
}
|
||||
|
||||
function statusType(status) {
|
||||
if (status === 'OK') return 'success'
|
||||
if (status === 'EMPTY') return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
function statusText(status) {
|
||||
if (status === 'OK') return '已识别'
|
||||
if (status === 'EMPTY') return '未识别到配置'
|
||||
if (status === 'MISSING') return '缺少工作表'
|
||||
return status || '-'
|
||||
}
|
||||
|
||||
function validateReady() {
|
||||
if (!selected.value.length) {
|
||||
proxy.$modal.msgError('请先上传价卡并勾选要保存的产品')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** 弹窗确认时回传勾选结果(创建页暂存,点完成再落库) */
|
||||
function getSelectionPayload() {
|
||||
return {
|
||||
overwrite: overwrite.value,
|
||||
sourceFile: preview.value?.sourceFile || '',
|
||||
supplierName: preview.value?.supplierName || '',
|
||||
effectiveDate: preview.value?.effectiveDate || '',
|
||||
products: selected.value.map(row => ({ ...row, productType: 'AR' }))
|
||||
}
|
||||
}
|
||||
|
||||
/** 按现有导入逻辑保存;basic 由创建页传入名称/备注 */
|
||||
async function submit(payload = {}) {
|
||||
if (!validateReady()) {
|
||||
throw new Error('NO_SELECTION')
|
||||
}
|
||||
const productsPayload = (payload.products && payload.products.length)
|
||||
? payload.products
|
||||
: selected.value.map(row => ({ ...row, productType: 'AR' }))
|
||||
const res = await saveLabelImport({
|
||||
priceSetId: payload.priceSetId || null,
|
||||
priceSetName: String(payload.priceSetName || '').trim(),
|
||||
priceSetNote: payload.priceSetNote || '',
|
||||
overwrite: payload.overwrite != null ? payload.overwrite : overwrite.value,
|
||||
products: productsPayload
|
||||
})
|
||||
const data = res.data || {}
|
||||
return {
|
||||
message: data.message || res.msg || '保存成功',
|
||||
priceSetId: data.priceSetId || payload.priceSetId
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
active.value = false
|
||||
stopProgress()
|
||||
})
|
||||
|
||||
defineExpose({ reset, validateReady, submit, getSelectionPayload, selectedCount })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.import-form { margin-bottom: 8px; }
|
||||
.form-tip { margin-left: 12px; color: #909399; font-size: 12px; }
|
||||
.upload-icon { font-size: 48px; color: #c0c4cc; }
|
||||
.upload-tip { color: #909399; font-size: 12px; margin-top: 6px; }
|
||||
.parse-progress { margin: 12px 0; }
|
||||
.parse-progress-msg { margin-top: 6px; color: #606266; font-size: 13px; }
|
||||
.preview-box { margin-top: 12px; }
|
||||
.preview-bar { display: flex; align-items: center; gap: 12px; margin: 10px 0; }
|
||||
.preview-hint { color: #909399; font-size: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="visible"
|
||||
:title="title"
|
||||
size="78%"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
@close="emit('update:visible', false)"
|
||||
>
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" clearable placeholder="产品名称 / 工作表 / 国家" style="width: 260px" />
|
||||
<el-button type="danger" plain :disabled="!selectedIds.length" @click="handleRemove" v-hasPermi="['price:label:remove']">移除</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredRows"
|
||||
border
|
||||
height="calc(100vh - 220px)"
|
||||
row-key="termId"
|
||||
@selection-change="onSelect"
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" />
|
||||
<el-table-column prop="productId" label="产品编号" width="90" align="center" />
|
||||
<el-table-column prop="productName" label="产品名称" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="supplierName" label="价卡归属" width="100" align="center" />
|
||||
<el-table-column prop="countryScope" label="国家/口岸" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="chargeType" label="费用类型" width="110" align="center" />
|
||||
<el-table-column prop="sheetName" label="报价工作表" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="versionNo" label="版本" width="70" align="center" />
|
||||
<el-table-column label="有效期" min-width="180" align="center">
|
||||
<template #default="{ row }">{{ termText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="termStatusTag(row.termStatus)" size="small">{{ termStatusText(row.termStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="weightCount" label="重量段" width="80" align="center" />
|
||||
<el-table-column prop="surchargeCount" label="附加费" width="80" align="center" />
|
||||
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openProduct(row)" v-hasPermi="['price:label:query']">查看</el-button>
|
||||
<el-button link type="danger" @click="handleRemove([row.productId])" v-hasPermi="['price:label:remove']">移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
|
||||
<pay-config-detail v-loading="detailLoading" :product="detail" :editable="false" />
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listLabelProducts, getLabelProduct, removeLabelProducts } from '@/api/price/label'
|
||||
import PayConfigDetail from '@/views/price/basic/pay/components/PayConfigDetail.vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
priceSetId: { type: [Number, String], default: null },
|
||||
version: { type: Object, default: null }
|
||||
})
|
||||
const emit = defineEmits(['update:visible', 'changed'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref([])
|
||||
const keyword = ref('')
|
||||
const selectedIds = ref([])
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detail = ref(null)
|
||||
|
||||
const title = computed(() => {
|
||||
const v = props.version || {}
|
||||
const label = v.versionLabel || (v.versionNo != null ? `V${v.versionNo}` : '')
|
||||
return label ? `版本明细 · ${label}` : '版本明细'
|
||||
})
|
||||
|
||||
const detailTitle = computed(() => {
|
||||
if (!detail.value) return '产品明细'
|
||||
const no = detail.value.versionNo ? `第 ${detail.value.versionNo} 版` : ''
|
||||
return `${detail.value.productName || '产品'} ${no}`.trim()
|
||||
})
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const kw = keyword.value.trim().toLowerCase()
|
||||
if (!kw) return rows.value
|
||||
return rows.value.filter(row => {
|
||||
const text = [row.productName, row.sheetName, row.countryScope, row.supplierName].join(' ').toLowerCase()
|
||||
return text.includes(kw)
|
||||
})
|
||||
})
|
||||
|
||||
function termText(row) {
|
||||
const start = row.termStart || row.effectiveDate || ''
|
||||
const end = row.termEnd || '不限'
|
||||
if (!start) return '未设置'
|
||||
return `${String(start).slice(0, 10)} ~ ${String(end).slice(0, 10)}`
|
||||
}
|
||||
|
||||
function termStatusText(status) {
|
||||
const map = { '0': '待生效', '1': '生效', '2': '已失效' }
|
||||
return map[String(status)] || '-'
|
||||
}
|
||||
|
||||
function termStatusTag(status) {
|
||||
const map = { '0': 'warning', '1': 'success', '2': 'danger' }
|
||||
return map[String(status)] || 'info'
|
||||
}
|
||||
|
||||
function onSelect(selection) {
|
||||
selectedIds.value = (selection || []).map(item => item.productId).filter(Boolean)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!props.priceSetId || props.version?.versionNo == null) {
|
||||
rows.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listLabelProducts(props.priceSetId, props.version.versionNo)
|
||||
rows.value = res.data || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openProduct(row) {
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
detail.value = null
|
||||
try {
|
||||
const res = await getLabelProduct(row.productId, row.termId)
|
||||
detail.value = res.data || null
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemove(ids) {
|
||||
const list = (Array.isArray(ids) ? ids : selectedIds.value).filter(Boolean)
|
||||
if (!list.length || !props.priceSetId) return
|
||||
proxy.$modal.confirm(`确认从本套餐移除选中的 ${list.length} 个产品?`).then(() => {
|
||||
return removeLabelProducts(props.priceSetId, list.join(','))
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('已移除')
|
||||
load()
|
||||
emit('changed')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
watch(() => [props.visible, props.priceSetId, props.version?.versionNo], ([vis]) => {
|
||||
if (vis) load()
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<el-card shadow="never" class="ver-card">
|
||||
<template #header>
|
||||
<div class="ver-head">
|
||||
<div class="ver-head-left">
|
||||
<span class="ver-title">套餐版本</span>
|
||||
<el-tag size="small" type="info" effect="plain" round>{{ versions.length }}</el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
:disabled="hasPending"
|
||||
v-hasPermi="['price:label:import']"
|
||||
@click="$emit('create-version')"
|
||||
>新建版本</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tabs v-model="statusTab" class="ver-tabs">
|
||||
<el-tab-pane :label="tabLabel('all')" name="all" />
|
||||
<el-tab-pane :label="tabLabel('0')" name="0" />
|
||||
<el-tab-pane :label="tabLabel('1')" name="1" />
|
||||
<el-tab-pane :label="tabLabel('2')" name="2" />
|
||||
</el-tabs>
|
||||
|
||||
<el-table :data="filteredVersions" border empty-text="暂无版本,完成导入后在此设置生效时间">
|
||||
<el-table-column label="版本号" width="100" align="center">
|
||||
<template #default="{ row }">{{ row.versionLabel || ('V' + (row.versionNo || 1)) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="生效时间" min-width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="$emit('open-details', row)">
|
||||
{{ formatDay(row.effectiveTime) || '未设置' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失效时间" min-width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDay(row.expireTime) || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="产品数" width="90" align="center">
|
||||
<template #default="{ row }">{{ row.productCount || 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ row.createTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="$emit('open-details', row)">查看详情</el-button>
|
||||
<el-button
|
||||
v-if="canEdit(row)"
|
||||
link
|
||||
type="primary"
|
||||
@click="$emit('edit', row)"
|
||||
v-hasPermi="['price:label:edit']"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
v-if="canReschedule(row)"
|
||||
link
|
||||
type="primary"
|
||||
@click="$emit('reschedule', row)"
|
||||
v-hasPermi="['price:label:edit']"
|
||||
>修改生效时间</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
versions: { type: Array, default: () => [] }
|
||||
})
|
||||
defineEmits(['edit', 'reschedule', 'open-details', 'create-version'])
|
||||
|
||||
const statusTab = ref('all')
|
||||
|
||||
const counts = computed(() => {
|
||||
const map = { all: 0, '0': 0, '1': 0, '2': 0 }
|
||||
;(props.versions || []).forEach(v => {
|
||||
map.all++
|
||||
const s = String(v?.status ?? '')
|
||||
if (map[s] != null) map[s]++
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
function tabLabel(key) {
|
||||
const labels = { all: '全部', '0': '待生效', '1': '已生效', '2': '已失效' }
|
||||
return `${labels[key]} (${counts.value[key] || 0})`
|
||||
}
|
||||
|
||||
const hasPending = computed(() => (props.versions || []).some(v => String(v.status) === '0'))
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
const list = props.versions || []
|
||||
if (statusTab.value === 'all') return list
|
||||
return list.filter(v => String(v.status) === statusTab.value)
|
||||
})
|
||||
|
||||
function formatDay(val) {
|
||||
if (!val) return ''
|
||||
return String(val).slice(0, 10)
|
||||
}
|
||||
|
||||
function statusText(status) {
|
||||
const map = { '0': '待生效', '1': '生效', '2': '已失效' }
|
||||
return map[String(status)] || '-'
|
||||
}
|
||||
|
||||
function statusTag(status) {
|
||||
const map = { '0': 'warning', '1': 'success', '2': 'danger' }
|
||||
return map[String(status)] || 'info'
|
||||
}
|
||||
|
||||
/** 待生效:可编辑价卡 */
|
||||
function canEdit(row) {
|
||||
return row && String(row.status) === '0'
|
||||
}
|
||||
|
||||
/** 仅待生效可改生效时间;已生效不可改 */
|
||||
function canReschedule(row) {
|
||||
return row && String(row.status) === '0'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ver-card { margin-bottom: 16px; }
|
||||
.ver-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
.ver-head-left { display: flex; align-items: center; gap: 8px; }
|
||||
.ver-title { font-weight: 600; }
|
||||
.ver-tabs { margin-bottom: 8px; }
|
||||
.ver-tabs :deep(.el-tabs__header) { margin-bottom: 8px; }
|
||||
</style>
|
||||
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div v-if="showBasic" class="create-section">
|
||||
<step-basic-info ref="basicRef" v-model="form.basic" />
|
||||
</div>
|
||||
|
||||
<div v-else class="create-section">
|
||||
<el-alert
|
||||
:title="pageBanner"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="create-section">
|
||||
<el-card shadow="never" class="step-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span>{{ cardTitle }}</span>
|
||||
<span class="header-tip">{{ headerTip }}</span>
|
||||
</div>
|
||||
<el-button type="primary" plain icon="Upload" @click="openImport" v-hasPermi="['price:label:import', 'price:label:add']">
|
||||
导入价卡
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
v-if="importMeta.sourceFile"
|
||||
:title="`${importMeta.sourceFile} 归属 ${importMeta.supplierName || '-'} 生效 ${importMeta.effectiveDate || '-'} 已选 ${productRows.length} 个产品`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="meta-alert"
|
||||
/>
|
||||
|
||||
<el-table v-loading="loadingProducts" :data="productRows" border size="small" max-height="420" empty-text="请点击右上角「导入价卡」">
|
||||
<el-table-column prop="productName" label="产品" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="sheetName" label="报价工作表" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="countryScope" label="国家/口岸" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="relatedSheet" label="关联分区表" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="weightCount" label="重量段" width="80" align="right" />
|
||||
<el-table-column prop="surchargeCount" label="附加费" width="80" align="right" />
|
||||
<el-table-column label="识别" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.parseStatus)" size="small">{{ statusText(row.parseStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库中" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.duplicated" type="warning" size="small">已存在</el-tag>
|
||||
<span v-else>新记录</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="openDetail(row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<div class="wizard-actions">
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="finish">完成</el-button>
|
||||
</div>
|
||||
|
||||
<label-import-dialog
|
||||
ref="importRef"
|
||||
:price-set-id="priceSetId"
|
||||
:auto-fill-on-parse="true"
|
||||
@confirm="onImportConfirm"
|
||||
/>
|
||||
|
||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body @closed="onDetailClosed">
|
||||
<pay-config-detail :product="detailProduct" :editable="detailEditing" />
|
||||
<template #footer>
|
||||
<el-button v-if="!detailEditing" type="primary" icon="Edit" @click="startEdit">修改</el-button>
|
||||
<el-button v-else type="primary" @click="applyDetail">保存</el-button>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="LabelPackageCreate">
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import StepBasicInfo from '../set/components/create/StepBasicInfo.vue'
|
||||
import { createEmptyBasicInfo } from '../set/utils/priceSet'
|
||||
import LabelImportDialog from './components/LabelImportDialog.vue'
|
||||
import PayConfigDetail from '@/views/price/basic/pay/components/PayConfigDetail.vue'
|
||||
import { getLabelPackage, getLabelProduct, listLabelProducts, saveLabelImport } from '@/api/price/label'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
const basicRef = ref()
|
||||
const importRef = ref()
|
||||
const saving = ref(false)
|
||||
const loadingProducts = ref(false)
|
||||
const form = reactive({
|
||||
basic: createEmptyBasicInfo()
|
||||
})
|
||||
const productRows = ref([])
|
||||
const importMeta = reactive({
|
||||
sourceFile: '',
|
||||
supplierName: '',
|
||||
effectiveDate: '',
|
||||
overwrite: false
|
||||
})
|
||||
const detailVisible = ref(false)
|
||||
const detailEditing = ref(false)
|
||||
const detailProduct = ref(null)
|
||||
const detailRowKey = ref('')
|
||||
const detailTitle = computed(() => detailProduct.value?.productName || detailProduct.value?.sheetName || '产品明细')
|
||||
|
||||
const priceSetId = computed(() => {
|
||||
const id = Number(route.query.priceSetId)
|
||||
return Number.isFinite(id) && id > 0 ? id : null
|
||||
})
|
||||
const versionNo = computed(() => {
|
||||
const no = Number(route.query.versionNo)
|
||||
return Number.isFinite(no) && no > 0 ? no : null
|
||||
})
|
||||
const newVersionMode = computed(() => String(route.query.newVersion || '') === '1' && !!priceSetId.value)
|
||||
const editVersionMode = computed(() => String(route.query.editVersion || '') === '1' && !!priceSetId.value && !!versionNo.value)
|
||||
const showBasic = computed(() => !newVersionMode.value && !editVersionMode.value)
|
||||
const cardTitle = computed(() => {
|
||||
if (editVersionMode.value) return `编辑版本 V${versionNo.value}`
|
||||
if (newVersionMode.value) return '新版本价卡'
|
||||
return '产品价卡'
|
||||
})
|
||||
const pageBanner = computed(() => {
|
||||
const name = form.basic.priceSetName || ''
|
||||
const code = form.basic.priceSetCode || ''
|
||||
if (editVersionMode.value) return `编辑待生效版本 V${versionNo.value}:${name}(${code})`
|
||||
return `新建版本:${name}(${code})`
|
||||
})
|
||||
const headerTip = computed(() => {
|
||||
if (editVersionMode.value) return '可重新导入或修改明细;完成后覆盖本待生效版本,不新增版本号'
|
||||
if (newVersionMode.value) return '导入后生成待生效新版本;生效时间请回详情页设置'
|
||||
return '打单价卡与应付独立;解析后回填到此表,查看后点修改可改明细'
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
const obj = (newVersionMode.value || editVersionMode.value) && priceSetId.value
|
||||
? { path: '/customer/labelDetail', query: { priceSetId: priceSetId.value } }
|
||||
: { path: '/customer/label' }
|
||||
proxy.$tab.closeOpenPage(obj)
|
||||
}
|
||||
|
||||
async function loadPackageHeader() {
|
||||
if ((!newVersionMode.value && !editVersionMode.value) || !priceSetId.value) return
|
||||
const res = await getLabelPackage(priceSetId.value)
|
||||
const data = res.data || {}
|
||||
form.basic.priceSetId = data.priceSetId
|
||||
form.basic.priceSetCode = data.priceSetCode || ''
|
||||
form.basic.priceSetName = data.priceSetName || ''
|
||||
form.basic.priceSetNote = data.priceSetNote || ''
|
||||
}
|
||||
|
||||
async function loadEditProducts() {
|
||||
if (!editVersionMode.value || !priceSetId.value || !versionNo.value) return
|
||||
loadingProducts.value = true
|
||||
try {
|
||||
const res = await listLabelProducts(priceSetId.value, versionNo.value)
|
||||
const list = res.data || []
|
||||
if (!list.length) {
|
||||
productRows.value = []
|
||||
return
|
||||
}
|
||||
const details = await Promise.all(list.map(async item => {
|
||||
try {
|
||||
const detailRes = await getLabelProduct(item.productId, item.termId)
|
||||
const detail = detailRes.data || item
|
||||
const bands = detail.weightBands || []
|
||||
const fees = detail.surcharges || []
|
||||
const extras = detail.extraLines || []
|
||||
return {
|
||||
...detail,
|
||||
rowKey: `${detail.sheetName || ''}-${detail.productName || ''}-${detail.termId || detail.productId || ''}`,
|
||||
weightCount: bands.length || detail.weightCount || 0,
|
||||
surchargeCount: fees.length || detail.surchargeCount || 0,
|
||||
extraCount: extras.length || detail.extraCount || 0,
|
||||
parseStatus: bands.length + fees.length + extras.length === 0 ? 'EMPTY' : 'OK',
|
||||
duplicated: true
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
...item,
|
||||
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${item.termId || item.productId || ''}`,
|
||||
parseStatus: 'OK',
|
||||
duplicated: true
|
||||
}
|
||||
}
|
||||
}))
|
||||
productRows.value = details
|
||||
importMeta.sourceFile = details[0]?.sourceFile || ''
|
||||
importMeta.supplierName = details[0]?.supplierName || ''
|
||||
importMeta.overwrite = true
|
||||
} finally {
|
||||
loadingProducts.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openImport() {
|
||||
importRef.value?.open()
|
||||
}
|
||||
|
||||
function onImportConfirm(payload) {
|
||||
productRows.value = payload?.products || []
|
||||
importMeta.sourceFile = payload?.sourceFile || ''
|
||||
importMeta.supplierName = payload?.supplierName || ''
|
||||
importMeta.effectiveDate = payload?.effectiveDate || ''
|
||||
importMeta.overwrite = editVersionMode.value ? true : !!payload?.overwrite
|
||||
}
|
||||
|
||||
function openDetail(row) {
|
||||
if (!row) return
|
||||
detailEditing.value = false
|
||||
detailRowKey.value = row.rowKey || `${row.sheetName || ''}-${row.productName || ''}`
|
||||
detailProduct.value = JSON.parse(JSON.stringify(row))
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
function startEdit() {
|
||||
detailEditing.value = true
|
||||
}
|
||||
|
||||
function onDetailClosed() {
|
||||
detailEditing.value = false
|
||||
detailProduct.value = null
|
||||
detailRowKey.value = ''
|
||||
}
|
||||
|
||||
function applyDetail() {
|
||||
const edited = detailProduct.value
|
||||
if (!edited) {
|
||||
detailVisible.value = false
|
||||
return
|
||||
}
|
||||
const key = detailRowKey.value
|
||||
const idx = productRows.value.findIndex(item => {
|
||||
const itemKey = item.rowKey || `${item.sheetName || ''}-${item.productName || ''}`
|
||||
return itemKey === key
|
||||
})
|
||||
if (idx < 0) {
|
||||
proxy.$modal.msgError('未找到对应产品行')
|
||||
return
|
||||
}
|
||||
const bands = edited.weightBands || []
|
||||
const fees = edited.surcharges || []
|
||||
const extras = edited.extraLines || []
|
||||
productRows.value.splice(idx, 1, {
|
||||
...edited,
|
||||
rowKey: productRows.value[idx].rowKey || key,
|
||||
weightCount: bands.length,
|
||||
surchargeCount: fees.length,
|
||||
extraCount: extras.length,
|
||||
parseStatus: bands.length + fees.length + extras.length === 0 ? 'EMPTY' : (edited.parseStatus || 'OK')
|
||||
})
|
||||
detailEditing.value = false
|
||||
detailVisible.value = false
|
||||
proxy.$modal.msgSuccess('已更新该产品明细')
|
||||
}
|
||||
|
||||
function statusType(status) {
|
||||
if (status === 'OK') return 'success'
|
||||
if (status === 'EMPTY') return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
function statusText(status) {
|
||||
if (status === 'OK') return '已识别'
|
||||
if (status === 'EMPTY') return '未识别到配置'
|
||||
if (status === 'MISSING') return '缺少工作表'
|
||||
return status || '-'
|
||||
}
|
||||
|
||||
async function finish() {
|
||||
if (showBasic.value) {
|
||||
const ok = await basicRef.value?.validate()
|
||||
if (!ok) {
|
||||
proxy.$modal.msgWarning('请先完善基本信息')
|
||||
return
|
||||
}
|
||||
} else if (!priceSetId.value) {
|
||||
proxy.$modal.msgWarning(editVersionMode.value ? '缺少套餐编号,无法编辑版本' : '缺少套餐编号,无法新建版本')
|
||||
return
|
||||
}
|
||||
if (!productRows.value.length) {
|
||||
proxy.$modal.msgWarning('请先导入并确认价卡产品')
|
||||
return
|
||||
}
|
||||
const tip = editVersionMode.value
|
||||
? `将覆盖待生效版本 V${versionNo.value} 的 ${productRows.value.length} 个产品,确认继续?`
|
||||
: newVersionMode.value
|
||||
? `将为套餐「${form.basic.priceSetName || ''}」新建待生效版本并保存 ${productRows.value.length} 个产品,确认继续?`
|
||||
: `将创建打单套餐并保存 ${productRows.value.length} 个应收产品,确认继续?`
|
||||
try {
|
||||
await proxy.$modal.confirm(tip)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
overwrite: editVersionMode.value ? true : importMeta.overwrite,
|
||||
products: productRows.value
|
||||
}
|
||||
if (editVersionMode.value) {
|
||||
payload.priceSetId = priceSetId.value
|
||||
payload.versionNo = versionNo.value
|
||||
} else if (newVersionMode.value) {
|
||||
payload.priceSetId = priceSetId.value
|
||||
} else {
|
||||
payload.priceSetName = form.basic.priceSetName
|
||||
payload.priceSetNote = form.basic.priceSetNote
|
||||
}
|
||||
const res = await saveLabelImport(payload)
|
||||
const data = res.data || {}
|
||||
ElMessageBox.close()
|
||||
proxy.$modal.msgSuccess(data.message || res.msg || '保存成功')
|
||||
const id = data.priceSetId || priceSetId.value
|
||||
if (id) {
|
||||
const obj = { path: '/customer/labelDetail', query: { priceSetId: id } }
|
||||
proxy.$tab.closeOpenPage(obj)
|
||||
} else {
|
||||
goBack()
|
||||
}
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch([priceSetId, versionNo, () => route.query.editVersion, () => route.query.newVersion], async () => {
|
||||
await loadPackageHeader()
|
||||
await loadEditProducts()
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.create-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
.header-tip {
|
||||
margin-left: 12px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
}
|
||||
.meta-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.wizard-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 8px 0 24px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-page-header class="page-header" @back="goBack">
|
||||
<template #content>
|
||||
<span class="title">{{ setInfo.priceSetName || '打单套餐详情' }}</span>
|
||||
<el-tag v-if="setInfo.priceSetCode" size="small" class="code-tag">{{ setInfo.priceSetCode }}</el-tag>
|
||||
<el-tag size="small" type="warning" effect="plain">打单 / LABEL</el-tag>
|
||||
<el-tag v-if="setInfo.status" size="small" :type="setStatusTag" effect="plain" class="status-tag">{{ setStatusText }}</el-tag>
|
||||
</template>
|
||||
<template #extra>
|
||||
<el-button type="primary" plain icon="Upload" @click="importRef.open()" v-hasPermi="['price:label:import']">导入价卡</el-button>
|
||||
</template>
|
||||
</el-page-header>
|
||||
|
||||
<set-customer-table :set="setInfo" @refresh="loadHeader" />
|
||||
|
||||
<label-version-table
|
||||
:versions="versions"
|
||||
@edit="openEditVersion"
|
||||
@reschedule="openReschedule"
|
||||
@open-details="openVersionDetails"
|
||||
@create-version="openCreateVersion"
|
||||
/>
|
||||
|
||||
<label-import-dialog ref="importRef" :price-set-id="priceSetId" @saved="reloadAll" />
|
||||
|
||||
<label-version-product-drawer
|
||||
v-model:visible="productDrawerVisible"
|
||||
:price-set-id="priceSetId"
|
||||
:version="activeVersion"
|
||||
@changed="reloadAll"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="activateVisible"
|
||||
title="修改生效时间"
|
||||
width="460px"
|
||||
append-to-body
|
||||
@closed="onActivateClosed"
|
||||
>
|
||||
<el-form ref="activateFormRef" :model="activateForm" :rules="activateRules" label-width="100px">
|
||||
<el-form-item label="版本号">
|
||||
<span>{{ activateForm.versionLabel }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="生效时间" prop="effectiveTime">
|
||||
<el-date-picker
|
||||
v-model="activateForm.effectiveTime"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择生效时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="activate-hint">仅待生效版本可修改;未到生效日仍为待生效,到日后自动按生效处理。</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="activateVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="activateSubmitting" @click="submitActivate">确认修改</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="LabelPackageDetail">
|
||||
import {
|
||||
getLabelPackage,
|
||||
listLabelVersions,
|
||||
activateLabelVersion,
|
||||
listLabelCustomer,
|
||||
addLabelCustomer,
|
||||
batchAddLabelCustomer,
|
||||
updateLabelCustomer,
|
||||
delLabelCustomer
|
||||
} from '@/api/price/label'
|
||||
import { priceSetApi } from '@/api/price/set'
|
||||
import { providePriceSetModule } from '../set/composables/usePriceSetModule'
|
||||
import SetCustomerTable from '../set/components/detail/SetCustomerTable.vue'
|
||||
import LabelImportDialog from './components/LabelImportDialog.vue'
|
||||
import LabelVersionTable from './components/LabelVersionTable.vue'
|
||||
import LabelVersionProductDrawer from './components/LabelVersionProductDrawer.vue'
|
||||
|
||||
const labelModuleApi = {
|
||||
...priceSetApi,
|
||||
listSetCustomer: listLabelCustomer,
|
||||
addSetCustomer: addLabelCustomer,
|
||||
batchAddSetCustomer: batchAddLabelCustomer,
|
||||
updateSetCustomer: updateLabelCustomer,
|
||||
delSetCustomer: delLabelCustomer
|
||||
}
|
||||
|
||||
providePriceSetModule({
|
||||
api: labelModuleApi,
|
||||
permPrefix: 'price:label',
|
||||
exclusiveQuote: false,
|
||||
onlyImportFresh: false,
|
||||
routes: Object.freeze({
|
||||
list: '/customer/label',
|
||||
create: '/customer/labelCreate',
|
||||
detail: '/customer/labelDetail'
|
||||
}),
|
||||
labels: Object.freeze({
|
||||
entity: '打单套餐',
|
||||
entityCode: '套餐编码',
|
||||
entityName: '套餐名称',
|
||||
createButton: '新建打单套餐',
|
||||
saveProgressTitle: '正在保存打单套餐',
|
||||
savedSuccess: '打单套餐已保存',
|
||||
noActiveVersion: '该打单套餐暂无生效版本',
|
||||
disableConfirm: '停用后该打单套餐不再参与计费命中,确认停用【{name}】?',
|
||||
enableConfirm: '启用后该打单套餐将重新参与计费命中,确认启用【{name}】?',
|
||||
basicEditTitle: '修改打单套餐基本信息',
|
||||
keywordPlaceholder: '套餐编码 / 名称',
|
||||
detailTitleFallback: '打单套餐详情'
|
||||
})
|
||||
})
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
const priceSetId = computed(() => Number(route.query.priceSetId) || null)
|
||||
const setInfo = reactive({
|
||||
priceSetId: null,
|
||||
priceSetCode: '',
|
||||
priceSetName: '',
|
||||
priceSetNote: '',
|
||||
status: '',
|
||||
customers: []
|
||||
})
|
||||
const versions = ref([])
|
||||
const importRef = ref()
|
||||
const productDrawerVisible = ref(false)
|
||||
const activeVersion = ref(null)
|
||||
|
||||
const setStatusText = computed(() => {
|
||||
const map = { D: '待启用', A: '审核中', E: '生效', R: '驳回', S: '已停用' }
|
||||
return map[setInfo.status] || setInfo.status || '-'
|
||||
})
|
||||
const setStatusTag = computed(() => {
|
||||
const map = { D: 'info', A: 'warning', E: 'success', R: 'danger', S: 'info' }
|
||||
return map[setInfo.status] || 'info'
|
||||
})
|
||||
|
||||
const activateVisible = ref(false)
|
||||
const activateSubmitting = ref(false)
|
||||
const activateFormRef = ref()
|
||||
const activateForm = reactive({
|
||||
versionNo: null,
|
||||
versionLabel: '',
|
||||
effectiveTime: undefined
|
||||
})
|
||||
const activateRules = {
|
||||
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
proxy.$router.push('/customer/label')
|
||||
}
|
||||
|
||||
async function loadHeader() {
|
||||
if (!priceSetId.value) return
|
||||
const res = await getLabelPackage(priceSetId.value)
|
||||
const data = res.data || {}
|
||||
setInfo.priceSetId = data.priceSetId
|
||||
setInfo.priceSetCode = data.priceSetCode || ''
|
||||
setInfo.priceSetName = data.priceSetName || ''
|
||||
setInfo.priceSetNote = data.priceSetNote || ''
|
||||
setInfo.status = data.status || ''
|
||||
setInfo.customers = data.customers || []
|
||||
}
|
||||
|
||||
async function loadVersions() {
|
||||
if (!priceSetId.value) return
|
||||
const res = await listLabelVersions(priceSetId.value)
|
||||
versions.value = res.data || []
|
||||
}
|
||||
|
||||
async function reloadAll() {
|
||||
await loadHeader()
|
||||
await loadVersions()
|
||||
}
|
||||
|
||||
function openVersionDetails(row) {
|
||||
activeVersion.value = row || null
|
||||
productDrawerVisible.value = true
|
||||
}
|
||||
|
||||
/** 跳转新建版本页;已有待生效版时按钮会禁用 */
|
||||
function openCreateVersion() {
|
||||
if (!priceSetId.value) return
|
||||
if ((versions.value || []).some(v => String(v.status) === '0')) {
|
||||
proxy.$modal.msgWarning('已有待生效版本,请先处理后再新建')
|
||||
return
|
||||
}
|
||||
proxy.$router.push({
|
||||
path: '/customer/labelCreate',
|
||||
query: { priceSetId: priceSetId.value, newVersion: '1' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 编辑待生效版本价卡 */
|
||||
function openEditVersion(row) {
|
||||
if (!priceSetId.value || !row?.versionNo) return
|
||||
proxy.$router.push({
|
||||
path: '/customer/labelCreate',
|
||||
query: {
|
||||
priceSetId: priceSetId.value,
|
||||
versionNo: row.versionNo,
|
||||
editVersion: '1'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openReschedule(row) {
|
||||
activateForm.versionNo = row.versionNo
|
||||
activateForm.versionLabel = row.versionLabel || ('V' + row.versionNo)
|
||||
activateForm.effectiveTime = row.effectiveTime ? String(row.effectiveTime).slice(0, 10) : undefined
|
||||
activateVisible.value = true
|
||||
}
|
||||
|
||||
function onActivateClosed() {
|
||||
activateForm.versionNo = null
|
||||
activateForm.versionLabel = ''
|
||||
activateForm.effectiveTime = undefined
|
||||
activateSubmitting.value = false
|
||||
}
|
||||
|
||||
function submitActivate() {
|
||||
activateFormRef.value?.validate(valid => {
|
||||
if (!valid) return
|
||||
activateSubmitting.value = true
|
||||
activateLabelVersion(priceSetId.value, activateForm.versionNo, {
|
||||
effectiveTime: activateForm.effectiveTime
|
||||
}).then(res => {
|
||||
const data = res.data || {}
|
||||
proxy.$modal.msgSuccess(data.message || res.msg || '操作成功')
|
||||
activateVisible.value = false
|
||||
reloadAll()
|
||||
}).finally(() => {
|
||||
activateSubmitting.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
watch(priceSetId, async (id) => {
|
||||
if (!id) return
|
||||
await reloadAll()
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-header { margin-bottom: 16px; }
|
||||
.title { font-weight: 600; margin-right: 8px; }
|
||||
.code-tag { margin-right: 8px; }
|
||||
.status-tag { margin-left: 8px; }
|
||||
.activate-hint { margin: -4px 0 0 100px; color: #909399; font-size: 12px; line-height: 1.5; }
|
||||
</style>
|
||||
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:search-columns="searchColumns"
|
||||
:query="queryList"
|
||||
row-key="priceSetId"
|
||||
>
|
||||
<template #toolbar>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleCreate" v-hasPermi="['price:label:add']">新建打单套餐</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<template #col-setType>
|
||||
<el-tag type="warning" size="small" effect="plain">打单 / LABEL</el-tag>
|
||||
</template>
|
||||
|
||||
<template #col-status="{ row }">
|
||||
<el-tag :type="statusTag(row)" size="small">{{ statusLabel(row) }}</el-tag>
|
||||
</template>
|
||||
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" @click="handleDetail(row)" v-hasPermi="['price:label:query']">详情</el-button>
|
||||
<el-button link type="primary" @click="openBasicEdit(row)" v-hasPermi="['price:label:edit']">修改</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'E'"
|
||||
link
|
||||
type="warning"
|
||||
@click="handleDisable(row)"
|
||||
v-hasPermi="['price:label:audit']"
|
||||
>停用</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'D' || row.status === 'S'"
|
||||
link
|
||||
type="success"
|
||||
@click="handleEnable(row)"
|
||||
v-hasPermi="['price:label:audit']"
|
||||
>启用</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
<el-dialog v-model="basicDialog.visible" :title="basicDialogTitle" width="520px" append-to-body destroy-on-close>
|
||||
<el-form ref="basicFormRef" :model="basicForm" :rules="basicRules" label-width="96px">
|
||||
<el-form-item label="套餐编码">
|
||||
<el-input :model-value="basicForm.priceSetCode" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="套餐名称" prop="priceSetName">
|
||||
<el-input v-model="basicForm.priceSetName" maxlength="50" show-word-limit placeholder="请输入套餐名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="说明备注" prop="priceSetNote">
|
||||
<el-input v-model="basicForm.priceSetNote" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="basicDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="basicDialog.saving" @click="submitBasicEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="LabelPackage">
|
||||
import { setStatusLabel, setStatusTagType } from '@/views/price/set/utils/priceSet'
|
||||
import {
|
||||
listLabelPackage,
|
||||
updateLabelBasic,
|
||||
disableLabelPackage,
|
||||
enableLabelPackage
|
||||
} from '@/api/price/label'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const isFirstActivate = ref(true)
|
||||
|
||||
const searchColumns = [
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
component: 'select',
|
||||
options: [
|
||||
{ label: '待启用', value: 'D' },
|
||||
{ label: '生效', value: 'E' },
|
||||
{ label: '已停用', value: 'S' }
|
||||
],
|
||||
placeholder: '全部状态',
|
||||
width: '140px'
|
||||
},
|
||||
{ label: '关键字', prop: 'keyword', component: 'input', placeholder: '套餐编码 / 名称', width: '200px' }
|
||||
]
|
||||
|
||||
const columns = [
|
||||
{ label: '套餐编码', prop: 'priceSetCode', width: 110 },
|
||||
{ label: '套餐名称', prop: 'priceSetName', minWidth: 160 },
|
||||
{ label: '类型', prop: 'setType', width: 140 },
|
||||
{ label: '产品数', prop: 'productCount', width: 90, align: 'center' },
|
||||
{ label: '说明', prop: 'priceSetNote', minWidth: 140 },
|
||||
{ label: '状态', prop: 'status', width: 90 },
|
||||
{ label: '操作', prop: 'action', width: 200, showInToolbar: false }
|
||||
]
|
||||
|
||||
function queryList(params) {
|
||||
return listLabelPackage(params)
|
||||
}
|
||||
|
||||
function statusLabel(row) {
|
||||
return setStatusLabel(row)
|
||||
}
|
||||
|
||||
function statusTag(row) {
|
||||
return setStatusTagType(row)
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
proxy.$router.push({ path: '/customer/labelCreate' })
|
||||
}
|
||||
|
||||
function handleDetail(row) {
|
||||
proxy.$router.push({ path: '/customer/labelDetail', query: { priceSetId: row.priceSetId } })
|
||||
}
|
||||
|
||||
const basicDialog = reactive({ visible: false, saving: false })
|
||||
const basicFormRef = ref()
|
||||
const basicForm = reactive({
|
||||
priceSetId: null,
|
||||
priceSetCode: '',
|
||||
priceSetName: '',
|
||||
priceSetNote: ''
|
||||
})
|
||||
const basicRules = {
|
||||
priceSetName: [{ required: true, message: '名称不能为空', trigger: 'blur' }]
|
||||
}
|
||||
const basicDialogTitle = '修改打单套餐基本信息'
|
||||
|
||||
function openBasicEdit(row) {
|
||||
if (!row || row.priceSetId == null) return
|
||||
basicForm.priceSetId = row.priceSetId
|
||||
basicForm.priceSetCode = row.priceSetCode || ''
|
||||
basicForm.priceSetName = row.priceSetName || ''
|
||||
basicForm.priceSetNote = row.priceSetNote || ''
|
||||
basicDialog.visible = true
|
||||
nextTick(() => basicFormRef.value && basicFormRef.value.clearValidate())
|
||||
}
|
||||
|
||||
function submitBasicEdit() {
|
||||
basicFormRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
basicDialog.saving = true
|
||||
updateLabelBasic({
|
||||
priceSetId: basicForm.priceSetId,
|
||||
priceSetName: basicForm.priceSetName,
|
||||
priceSetNote: basicForm.priceSetNote
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('保存成功')
|
||||
basicDialog.visible = false
|
||||
tableRef.value?.refresh()
|
||||
}).finally(() => {
|
||||
basicDialog.saving = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleDisable(row) {
|
||||
proxy.$modal.confirm(`停用后该打单套餐不再参与计费命中,确认停用【${row.priceSetName}】?`).then(() => {
|
||||
return disableLabelPackage(row.priceSetId)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('已停用')
|
||||
tableRef.value?.refresh()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function handleEnable(row) {
|
||||
proxy.$modal.confirm(`启用后该打单套餐将重新参与计费命中,确认启用【${row.priceSetName}】?`).then(() => {
|
||||
return enableLabelPackage(row.priceSetId)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('已启用')
|
||||
tableRef.value?.refresh()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
if (isFirstActivate.value) {
|
||||
isFirstActivate.value = false
|
||||
return
|
||||
}
|
||||
tableRef.value?.reload()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user