Refactor billing options retrieval to include data permissions and add effective date handling for product imports

This commit is contained in:
wuweihua
2026-10-09 09:27:28 +08:00
parent fea34d1d10
commit c95a9861cc
42 changed files with 984 additions and 219 deletions
@@ -111,21 +111,32 @@ public class BillingController extends BaseController
@GetMapping("/bill/orderNos")
public AjaxResult orderNoOptions(@RequestParam(value = "keyword", required = false) String keyword)
{
return success(prcOrderBillService.listOrderNoOptions(keyword));
return success(prcOrderBillService.listOrderNoOptions(buildBillOptionQuery(keyword)));
}
@PreAuthorize("@ss.hasPermi('price:billing:bill')")
@GetMapping("/bill/trackingNos")
public AjaxResult trackingNoOptions(@RequestParam(value = "keyword", required = false) String keyword)
{
return success(prcOrderBillService.listTrackingNoOptions(keyword));
return success(prcOrderBillService.listTrackingNoOptions(buildBillOptionQuery(keyword)));
}
@PreAuthorize("@ss.hasPermi('price:billing:bill')")
@GetMapping("/bill/transferNos")
public AjaxResult transferNoOptions(@RequestParam(value = "keyword", required = false) String keyword)
{
return success(prcOrderBillService.listTransferNoOptions(keyword));
return success(prcOrderBillService.listTransferNoOptions(buildBillOptionQuery(keyword)));
}
/** 下拉搜索:keyword 放入 params,供 @DataScope 与 Mapper 使用 */
private PrcOrderBill buildBillOptionQuery(String keyword)
{
PrcOrderBill query = new PrcOrderBill();
if (StringUtils.isNotEmpty(keyword))
{
query.getParams().put("optionKeyword", keyword.trim());
}
return query;
}
@PreAuthorize("@ss.hasPermi('price:billing:bill')")
@@ -145,6 +145,15 @@ public class PriceSetController extends BaseController
return success(newPsvId);
}
@PreAuthorize("@ss.hasPermi('price:set:add')")
@Log(title = "Price set version create", businessType = BusinessType.INSERT)
@PostMapping("/version/create/{priceSetId}")
public AjaxResult createVersion(@PathVariable Long priceSetId)
{
Long newPsvId = priceSetService.createVersion(priceSetId, getUsername());
return success(newPsvId);
}
@PreAuthorize("@ss.hasPermi('price:set:edit')")
@Log(title = "Price set version delete", businessType = BusinessType.DELETE)
@DeleteMapping("/version/{psvId}")
@@ -5,6 +5,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -49,6 +50,17 @@ public class ProductController extends BaseController
return getDataTable(list);
}
/**
* 产品下拉选项(非分页)。
* 供计费列表、客户绑定等场景选择产品;不做权限控制(仍需登录)。
*/
@GetMapping("/optionselect")
public AjaxResult optionselect(BmsProduct product)
{
List<BmsProduct> list = productService.selectBmsProductList(product);
return success(list);
}
@PreAuthorize("@ss.hasPermi('price:product:import')")
@GetMapping("/import/task/{taskId}")
public AjaxResult getImportTask(@PathVariable String taskId)
@@ -114,13 +126,15 @@ public class ProductController extends BaseController
/**
* Async import Weishi B2C workbook. Returns taskId; poll /import/task/{taskId}.
* effectiveDate is required: written to product / product_country; previous open rows expire at this date.
*/
@Log(title = "\u4ea7\u54c1\u5217\u8868", businessType = BusinessType.IMPORT)
@PreAuthorize("@ss.hasPermi('price:product:import')")
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
public AjaxResult importData(MultipartFile file, boolean updateSupport,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date effectiveDate) throws Exception
{
String taskId = productService.submitImportProductExcelAsync(file, updateSupport, getUsername());
String taskId = productService.submitImportProductExcelAsync(file, updateSupport, getUsername(), effectiveDate);
Map<String, Object> payload = new HashMap<String, Object>();
payload.put("taskId", taskId);
return success(payload);
@@ -128,13 +142,16 @@ public class ProductController extends BaseController
/**
* Async single-country cost-sheet import. Returns taskId; poll /import/task/{taskId}.
* effectiveDate is required: written to product / product_country; previous open rows expire at this date.
*/
@Log(title = "\u4ea7\u54c1\u5217\u8868", businessType = BusinessType.IMPORT)
@PreAuthorize("@ss.hasPermi('price:product:import')")
@PostMapping("/importCountryData")
public AjaxResult importCountryData(MultipartFile file, String countryCode) throws Exception
public AjaxResult importCountryData(MultipartFile file, String countryCode,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date effectiveDate) throws Exception
{
String taskId = productService.submitImportCountryProductExcelAsync(file, countryCode, getUsername());
String taskId = productService.submitImportCountryProductExcelAsync(file, countryCode, getUsername(),
effectiveDate);
Map<String, Object> payload = new HashMap<String, Object>();
payload.put("taskId", taskId);
return success(payload);
@@ -111,13 +111,14 @@ price.zone.countryCode.empty=国家编码不能为空,请检查 region.country
price.product.codeDate.exists=同一产品代码与生效日期已存在,不能覆盖历史数据
price.product.effectiveDate.empty=生效日期不能为空
price.product.import.effEmpty=缺少生效日期,已跳过
price.product.import.effRequired=请选择生效时间
price.product.import.fileEmpty=上传文件不能为空
price.product.import.fileType=仅支持 xls / xlsx 文件
price.product.import.sheetNotFound=未找到「产品目录」sheet,请上传威狮B2C小包价表格格式文件
price.product.import.noData=产品目录无有效数据行
price.product.import.regionEmpty=region 无可用国家数据,请先同步/维护国家表后再导入
price.product.import.partialFail=导入完成但存在失败项:新增 {0},跳过 {1};失败明细:{2}
price.product.import.effTooEarly=生效日期 {0} 早于当前开放版 {1},请使用更晚的生效日期导入
price.product.import.effTooEarly=产品{0}({1})导入生效时间{2}早于该产品上一版生效时间{3},不允许导入
price.product.import.needFeeAlter=请先执行 SQL:bms-back/sql/price/product_country_fee_alter.sql(缺少运费/挂号费字段)
price.product.import.needExpireAlter=请先执行 SQL:bms-back/sql/price/product_country_expire_alter.sql(缺少失效日期/版本唯一键)
price.product.import.needCodeEffUnique=无法新增「同代码、不同生效日」的产品。请先执行 SQL:bms-back/sql/price/product_code_eff_unique_alter.sql(去掉 product_code 单列唯一,改为代码+生效日唯一)
@@ -181,10 +182,12 @@ price.bill.fee.alreadyBilled=费用已生成账单,不可编辑或删除
price.bill.fee.idRequired=费用ID不能为空
price.set.version.effectiveTime.empty=版本生效时间不能为空
price.set.version.effectiveTime.beforePrev=生效时间不能早于上一版本的生效时间
price.set.version.notExists=套餐版本不存在
price.set.version.activate.statusNotAllowed=仅草稿或待生效版本可设置生效时间
price.set.version.delete.statusNotAllowed=仅草稿版本可删除
price.set.version.copy.draftExists=已有草稿版本,请先启动或删除后再复制
price.set.version.create.draftExists=已有草稿版本,请先启动或删除后再新建
price.set.save.task.notExists=保存任务不存在或已过期
price.set.library.empty=产品库无可用价格明细,请检查产品国家配置
price.set.detail.duplicate.fixed=产品{0} 国家{1} 重量段{2} {3}(共{4}条)
@@ -23,9 +23,9 @@ public class BmsProduct extends BaseEntity
private String updateNote;
/** TAX_INCLUDED / TAX_EXCLUDED */
private String taxMode;
@JsonFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date effectiveDate;
@JsonFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expireDate;
/** 0 pending, 1 active, 2 expired */
public static final String STATUS_PENDING = "0";
@@ -31,10 +31,10 @@ public class BmsProductCountry implements Serializable
private String sizeLimit;
private String lastMile;
private String leadTime;
@JsonFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date effectiveDate;
/** null=open; closed to next import effective_date */
@JsonFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expireDate;
/** 0 pending, 1 active, 2 expired */
private String status;
@@ -20,6 +20,10 @@ public class PrcIncomeBillFee implements Serializable
private String feeName;
private Long scId;
private BigDecimal amount;
/** 产品价(目录价,基础运费/挂号费明细对应分摊) */
private BigDecimal productAmount;
/** 红线价(预留) */
private BigDecimal redlineAmount;
private String currency;
private String roundMode;
private String remark;
@@ -37,6 +37,8 @@ public class PrcOrderBill extends BaseEntity
private String settlementManCode;
/** 客服员 */
private String serviceMan;
/** 客服员编码 */
private String serviceManCode;
private Long customerId;
private String customerCode;
private String customerName;
@@ -33,6 +33,8 @@ public class TmsPriceBillRequest extends TmsPriceQuoteRequest
private String settlementManCode;
/** 客服员 */
private String serviceMan;
/** 客服员编码 */
private String serviceManCode;
private String customerName;
private Integer pieces;
private BigDecimal actualWeight;
@@ -16,6 +16,10 @@ public class TmsPriceQuoteFeeLine implements Serializable
/** surcharge config id, 0 for freight/registration */
private Long scId;
private BigDecimal amount;
/** 产品价(目录价分摊) */
private BigDecimal productAmount;
/** 红线价(预留) */
private BigDecimal redlineAmount;
private String currency;
private String roundMode;
private String remark;
@@ -91,6 +91,8 @@ public class TmsPriceQuoteResult implements Serializable
private String settlementManCode;
/** 客服员 */
private String serviceMan;
/** 客服员编码 */
private String serviceManCode;
private String customerCode;
private String customerName;
/** ACTIVE / MISSING / DISABLED */
@@ -20,14 +20,14 @@ public interface PrcOrderBillMapper
public List<PrcOrderBill> selectPrcOrderBillList(PrcOrderBill bill);
/** 订单号下拉搜索:按关键词模糊匹配,返回去重后的订单号(按最近创建排序,限量返回) */
public List<String> selectOrderNoOptions(@Param("keyword") String keyword, @Param("limit") int limit);
/** 订单号下拉搜索(带数据权限) */
public List<String> selectOrderNoOptions(@Param("query") PrcOrderBill query, @Param("limit") int limit);
/** 运单号下拉搜索:按关键词模糊匹配,返回去重后的运单号(按最近创建排序,限量返回) */
public List<String> selectTrackingNoOptions(@Param("keyword") String keyword, @Param("limit") int limit);
/** 运单号下拉搜索(带数据权限) */
public List<String> selectTrackingNoOptions(@Param("query") PrcOrderBill query, @Param("limit") int limit);
/** 转单号下拉搜索:按关键词模糊匹配,返回去重后的转单号(按最近创建排序,限量返回) */
public List<String> selectTransferNoOptions(@Param("keyword") String keyword, @Param("limit") int limit);
/** 转单号下拉搜索(带数据权限) */
public List<String> selectTransferNoOptions(@Param("query") PrcOrderBill query, @Param("limit") int limit);
/** 应收账单生成读取指定客户截止日期前仍存在未出账费用的订单计费快照。 */
public List<PrcOrderBill> selectPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
@@ -26,11 +26,12 @@ public class OracleOrderBillMapper
req.setOrderNo(firstStr(row, "ORDER_ID", "ORG_ORDER_ID", "TRANSFER_ORDER_ID"));
req.setTrackingNo(firstStr(row, "ORG_ORDER_ID", "TRACK_ID", "BARCODE_NUMBER", "CUSTOMER_TRACK_ID"));
req.setTransferNo(firstStr(row, "TRANSFER_ORDER_ID"));
req.setBelongSaler(firstStr(row, "BELONG_SALER_OLD"));
// Oracle 人员字段为中文名;编码在落库前按 sys_user.nick_name 反查
req.setBelongSaler(firstStr(row, "BELONG_SALER_OLD", "BELONG_SALER_NAME", "BELONG_SALER"));
req.setBelongSalerCode(firstStr(row, "BELONG_SALER_CODE", "SALER_CODE"));
req.setSettlementMan(firstStr(row, "SETTLEMENT_MAN_OLD"));
req.setSettlementMan(firstStr(row, "SETTLEMENT_MAN_OLD", "SETTLEMENT_MAN_NAME", "SETTLEMENT_MAN"));
req.setSettlementManCode(firstStr(row, "SETTLEMENT_MAN_CODE", "SETTLEMENT_CODE"));
req.setServiceMan(firstStr(row, "SERVICE_MAN_OLD"));
req.setServiceMan(firstStr(row, "SERVICE_MAN_OLD", "SERVICE_MAN_NAME", "SERVICE_MAN"));
req.setUpstreamRow(row);
req.setCustomerCode(firstStr(row, "CUSTOMER_CODE"));
req.setCustomerName(firstStr(row, "CUSTOMER_NAME"));
@@ -32,24 +32,24 @@ public interface IBmsProductService
public List<BmsProductCountry> selectCountryListByProductCode(String productCode, Date effectiveDate, Long productId);
public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception;
public String importProductExcel(MultipartFile file, boolean updateSupport, String operName, Date effectiveDate) throws Exception;
public String importCountryProductExcel(MultipartFile file, String countryCode, String operName) throws Exception;
public String importCountryProductExcel(MultipartFile file, String countryCode, String operName, Date effectiveDate) throws Exception;
/** Submit async all-country import; returns taskId for polling getImportTask. */
public String submitImportProductExcelAsync(MultipartFile file, boolean updateSupport, String operName) throws Exception;
public String submitImportProductExcelAsync(MultipartFile file, boolean updateSupport, String operName, Date effectiveDate) throws Exception;
/** Submit async single-country import; returns taskId for polling getImportTask. */
public String submitImportCountryProductExcelAsync(MultipartFile file, String countryCode, String operName) throws Exception;
public String submitImportCountryProductExcelAsync(MultipartFile file, String countryCode, String operName, Date effectiveDate) throws Exception;
/** Poll Redis progress for product import task. */
public BmsProductImportTask getImportTask(String taskId);
/** Sync import from bytes (used by async runner; taskId may be null). */
public String importProductExcel(byte[] bytes, String filename, boolean updateSupport, String operName, String taskId) throws Exception;
public String importProductExcel(byte[] bytes, String filename, boolean updateSupport, String operName, String taskId, Date effectiveDate) throws Exception;
/** Sync country import from bytes (used by async runner; taskId may be null). */
public String importCountryProductExcel(byte[] bytes, String filename, String countryCode, String operName, String taskId) throws Exception;
public String importCountryProductExcel(byte[] bytes, String filename, String countryCode, String operName, String taskId, Date effectiveDate) throws Exception;
public java.util.Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(MultipartFile file) throws Exception;
@@ -23,14 +23,14 @@ public interface IPrcOrderBillService
List<PrcOrderBill> selectBillList(PrcOrderBill query);
/** 订单号下拉搜索选项(模糊匹配,去重限量) */
List<String> listOrderNoOptions(String keyword);
/** 订单号下拉搜索选项(模糊匹配,去重限量,带数据权限) */
List<String> listOrderNoOptions(PrcOrderBill query);
/** 运单号下拉搜索选项(模糊匹配,去重限量) */
List<String> listTrackingNoOptions(String keyword);
/** 运单号下拉搜索选项(模糊匹配,去重限量,带数据权限) */
List<String> listTrackingNoOptions(PrcOrderBill query);
/** 转单号下拉搜索选项(模糊匹配,去重限量) */
List<String> listTransferNoOptions(String keyword);
/** 转单号下拉搜索选项(模糊匹配,去重限量,带数据权限) */
List<String> listTransferNoOptions(PrcOrderBill query);
TmsPriceQuoteResult getBill(String billId);
@@ -51,6 +51,12 @@ public interface IPrcPriceSetService
*/
public Long copyVersion(Long sourcePsvId, String createBy);
/**
* Create an empty draft version under the package (no detail copy).
* @return new psvId
*/
public Long createVersion(Long priceSetId, String createBy);
/** Delete a draft version and its details / surcharge snapshots */
public int deleteVersion(Long psvId, String updateBy);
@@ -244,21 +244,25 @@ public class BmsProductServiceImpl implements IBmsProductService
}
@Override
public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception
public String importProductExcel(MultipartFile file, boolean updateSupport, String operName, Date effectiveDate)
throws Exception
{
validateImportFile(file);
return importProductExcel(file.getBytes(), file.getOriginalFilename(), updateSupport, operName, null);
return importProductExcel(file.getBytes(), file.getOriginalFilename(), updateSupport, operName, null,
effectiveDate);
}
@Override
public String submitImportProductExcelAsync(MultipartFile file, boolean updateSupport, String operName) throws Exception
public String submitImportProductExcelAsync(MultipartFile file, boolean updateSupport, String operName,
Date effectiveDate) throws Exception
{
validateImportFile(file);
Date eff = requireImportEffectiveDate(effectiveDate);
byte[] bytes = file.getBytes();
String filename = file.getOriginalFilename();
String taskId = IdUtils.fastSimpleUUID();
importAsyncRunner.put(taskId, BmsProductImportTask.STATUS_PENDING, 0, "queued", null);
importAsyncRunner.runImportAll(taskId, bytes, filename, updateSupport, operName);
importAsyncRunner.runImportAll(taskId, bytes, filename, updateSupport, operName, eff);
return taskId;
}
@@ -273,9 +277,11 @@ public class BmsProductServiceImpl implements IBmsProductService
}
@Override
public String importProductExcel(byte[] bytes, String filename, boolean updateSupport, String operName, String taskId) throws Exception
public String importProductExcel(byte[] bytes, String filename, boolean updateSupport, String operName,
String taskId, Date effectiveDate) throws Exception
{
validateImportBytes(bytes, filename);
Date forcedEff = requireImportEffectiveDate(effectiveDate);
reportImportProgress(taskId, 10, "parsing excel");
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
@@ -293,6 +299,8 @@ public class BmsProductServiceImpl implements IBmsProductService
}
products = dedupeCatalogProducts(products);
reportImportProgress(taskId, 20, "parsing excel");
// Pre-check all products: reject whole import before any write if any is earlier than prev version
assertImportProductsEffectiveNotBeforePrev(products, forcedEff);
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
if (regionByNameCn.isEmpty())
@@ -319,8 +327,9 @@ public class BmsProductServiceImpl implements IBmsProductService
TransactionStatus tx = transactionManager.getTransaction(def);
try
{
product.setEffectiveDate(forcedEff);
int[] nums = importOneProduct(workbook, product, operName, regionByNameCn, euRegions,
zoneByCountryAndCode, unmatchedCountries);
zoneByCountryAndCode, unmatchedCountries, forcedEff);
transactionManager.commit(tx);
successNum += nums[0];
skipNum += nums[1];
@@ -357,32 +366,38 @@ public class BmsProductServiceImpl implements IBmsProductService
}
@Override
public String importCountryProductExcel(MultipartFile file, String countryCode, String operName) throws Exception
public String importCountryProductExcel(MultipartFile file, String countryCode, String operName, Date effectiveDate)
throws Exception
{
validateImportFile(file);
return importCountryProductExcel(file.getBytes(), file.getOriginalFilename(), countryCode, operName, null);
return importCountryProductExcel(file.getBytes(), file.getOriginalFilename(), countryCode, operName, null,
effectiveDate);
}
@Override
public String submitImportCountryProductExcelAsync(MultipartFile file, String countryCode, String operName) throws Exception
public String submitImportCountryProductExcelAsync(MultipartFile file, String countryCode, String operName,
Date effectiveDate) throws Exception
{
validateImportFile(file);
if (StringUtils.isEmpty(countryCode))
{
throw new ServiceException(MessageUtils.message("price.product.import.countryRequired"));
}
Date eff = requireImportEffectiveDate(effectiveDate);
byte[] bytes = file.getBytes();
String filename = file.getOriginalFilename();
String taskId = IdUtils.fastSimpleUUID();
importAsyncRunner.put(taskId, BmsProductImportTask.STATUS_PENDING, 0, "queued", null);
importAsyncRunner.runImportCountry(taskId, bytes, filename, countryCode, operName);
importAsyncRunner.runImportCountry(taskId, bytes, filename, countryCode, operName, eff);
return taskId;
}
@Override
public String importCountryProductExcel(byte[] bytes, String filename, String countryCode, String operName, String taskId) throws Exception
public String importCountryProductExcel(byte[] bytes, String filename, String countryCode, String operName,
String taskId, Date effectiveDate) throws Exception
{
validateImportBytes(bytes, filename);
Date forcedEff = requireImportEffectiveDate(effectiveDate);
if (StringUtils.isEmpty(countryCode))
{
throw new ServiceException(MessageUtils.message("price.product.import.countryRequired"));
@@ -410,6 +425,8 @@ public class BmsProductServiceImpl implements IBmsProductService
products = dedupeCatalogProducts(products);
Map<String, PriceBlock> priceBlocks = parseCountryPriceBlocks(workbook);
reportImportProgress(taskId, 20, "parsing excel");
// Pre-check all products: reject whole import before any write if any is earlier than prev version
assertImportProductsEffectiveNotBeforePrev(products, forcedEff);
int successNum = 0;
int skipNum = 0;
@@ -437,11 +454,12 @@ public class BmsProductServiceImpl implements IBmsProductService
}
product.setDestRegion(countryLabel);
product.setSourceSheet(block.sheetName);
product.setEffectiveDate(forcedEff);
if (StringUtils.isEmpty(product.getTaxMode()))
{
product.setTaxMode(resolveTaxMode(product.getProductName()));
}
List<BmsProductCountry> countries = stampSelectedCountry(block.rows, region, product.getEffectiveDate());
List<BmsProductCountry> countries = stampSelectedCountry(block.rows, region, forcedEff);
int[] nums = persistImportedProduct(product, countries, operName);
transactionManager.commit(tx);
successNum += nums[0];
@@ -594,7 +612,7 @@ public class BmsProductServiceImpl implements IBmsProductService
/** @return int[]{added, skipped, countryRows} */
private int[] importOneProduct(Workbook workbook, BmsProduct product, String operName,
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
Map<String, PrcZone> zoneByCountryAndCode, Set<String> unmatchedCountries)
Map<String, PrcZone> zoneByCountryAndCode, Set<String> unmatchedCountries, Date forcedEffectiveDate)
{
product.setSourceSheet(product.getProductName());
product.setCreateBy(operName);
@@ -614,13 +632,11 @@ public class BmsProductServiceImpl implements IBmsProductService
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
productSheet.getSheetName()));
}
Date sheetEff = resolveImportEffectiveDate(countries);
Date newEff = toDateOnly(sheetEff != null ? sheetEff : product.getEffectiveDate());
product.setEffectiveDate(newEff);
if (newEff == null)
{
throw new ServiceException(MessageUtils.message("price.product.import.effEmpty"));
}
// UI-selected effective date wins over Excel; previous open product/country rows expire at this date
Date newEff = requireImportEffectiveDate(forcedEffectiveDate != null ? forcedEffectiveDate
: product.getEffectiveDate());
applyForcedEffectiveDate(product, countries, newEff);
assertImportEffectiveNotBeforePrev(product, newEff);
BmsProduct existing = productMapper.selectBmsProductByCodeAndDate(
product.getProductCode(), newEff);
@@ -657,7 +673,7 @@ public class BmsProductServiceImpl implements IBmsProductService
* - expire all earlier open weight bands under that country
* - delete same-day open weight bands (unique key blocks insert if only expired)
* - insert Excel weight bands only (no copy / merge of old bands)
* Reject when newEff &lt; last open effective_date.
* Reject when newEff is earlier than the product's previous version effective time.
*/
private int saveCountries(BmsProduct product, List<BmsProductCountry> countries)
{
@@ -667,22 +683,16 @@ public class BmsProductServiceImpl implements IBmsProductService
}
String productCode = product.getProductCode();
Long productId = product.getProductId();
Date newEff = toDateOnly(resolveImportEffectiveDate(countries));
Date newEff = normalizeToHour(product.getEffectiveDate());
if (newEff == null)
{
newEff = toDateOnly(product.getEffectiveDate());
newEff = normalizeToHour(resolveImportEffectiveDate(countries));
}
if (newEff == null)
{
throw new ServiceException(MessageUtils.message("price.product.import.effEmpty"));
}
Date lastOpenEff = toDateOnly(countryMapper.selectMaxOpenEffectiveDate(productCode));
if (lastOpenEff != null && newEff.before(lastOpenEff))
{
throw new ServiceException(MessageUtils.message("price.product.import.effTooEarly",
formatDate(newEff), formatDate(lastOpenEff)));
throw new ServiceException(MessageUtils.message("price.product.import.effRequired"));
}
assertImportEffectiveNotBeforePrev(product, newEff);
Set<String> replacedCountries = new HashSet<>();
String rowStatus = resolveStoredStatus(newEff, null);
@@ -776,6 +786,15 @@ public class BmsProductServiceImpl implements IBmsProductService
return new SimpleDateFormat("yyyy-MM-dd").format(d);
}
private String formatDateTime(Date d)
{
if (d == null)
{
return "-";
}
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(d);
}
private Date toDateOnly(Date d)
{
if (d == null)
@@ -792,17 +811,35 @@ public class BmsProductServiceImpl implements IBmsProductService
}
}
/** 0 pending / 1 active / 2 expired from dates vs today */
/** Truncate to hour precision (mm:ss = 00). */
private Date normalizeToHour(Date d)
{
if (d == null)
{
return null;
}
try
{
String s = new SimpleDateFormat("yyyy-MM-dd HH").format(d) + ":00:00";
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(s);
}
catch (Exception e)
{
return d;
}
}
/** 0 pending / 1 active / 2 expired from datetime vs now */
private String resolveStoredStatus(Date effectiveDate, Date expireDate)
{
Date today = toDateOnly(new Date());
Date expire = toDateOnly(expireDate);
Date eff = toDateOnly(effectiveDate);
if (expire != null && today != null && !expire.after(today))
Date now = new Date();
Date expire = expireDate;
Date eff = effectiveDate;
if (expire != null && !expire.after(now))
{
return BmsProduct.STATUS_EXPIRED;
}
if (eff == null || (today != null && eff.after(today)))
if (eff == null || eff.after(now))
{
return BmsProduct.STATUS_PENDING;
}
@@ -1893,6 +1930,85 @@ public class BmsProductServiceImpl implements IBmsProductService
return out;
}
/** Require import effective time (hour precision). */
private Date requireImportEffectiveDate(Date effectiveDate)
{
Date d = normalizeToHour(effectiveDate);
if (d == null)
{
throw new ServiceException(MessageUtils.message("price.product.import.effRequired"));
}
return d;
}
/**
* Reject import when selected effective time is earlier than the product's latest previous version.
* Same effective time (re-import that version) is allowed.
*/
private void assertImportEffectiveNotBeforePrev(BmsProduct product, Date newEff)
{
if (product == null || StringUtils.isEmpty(product.getProductCode()) || newEff == null)
{
return;
}
BmsProduct latest = productMapper.selectBmsProductByCode(product.getProductCode());
if (latest == null || latest.getEffectiveDate() == null)
{
return;
}
Date prevEff = normalizeToHour(latest.getEffectiveDate());
if (prevEff == null || !newEff.before(prevEff))
{
return;
}
String code = product.getProductCode();
String name = StringUtils.isNotEmpty(product.getProductName())
? product.getProductName()
: (StringUtils.isNotEmpty(latest.getProductName()) ? latest.getProductName() : "-");
throw new ServiceException(MessageUtils.message("price.product.import.effTooEarly",
code, name, formatDateTime(newEff), formatDateTime(prevEff)));
}
/** Batch pre-check: fail whole import before any DB write. */
private void assertImportProductsEffectiveNotBeforePrev(List<BmsProduct> products, Date forcedEff)
{
Date newEff = requireImportEffectiveDate(forcedEff);
if (products == null || products.isEmpty())
{
return;
}
for (BmsProduct product : products)
{
if (product == null)
{
continue;
}
product.setEffectiveDate(newEff);
assertImportEffectiveNotBeforePrev(product, newEff);
}
}
/** Stamp product + all country detail rows with the same effective date. */
private void applyForcedEffectiveDate(BmsProduct product, List<BmsProductCountry> countries, Date newEff)
{
Date eff = requireImportEffectiveDate(newEff);
if (product != null)
{
product.setEffectiveDate(eff);
}
if (countries == null)
{
return;
}
for (BmsProductCountry c : countries)
{
if (c != null)
{
c.setEffectiveDate(eff);
}
}
}
/** @return int[]{added, skipped, countryRows} */
private int[] persistImportedProduct(BmsProduct product, List<BmsProductCountry> countries, String operName)
{
@@ -1902,16 +2018,9 @@ public class BmsProductServiceImpl implements IBmsProductService
{
product.setSourceSheet(product.getProductName());
}
Date newEff = toDateOnly(product.getEffectiveDate());
if (newEff == null)
{
newEff = toDateOnly(resolveImportEffectiveDate(countries));
}
product.setEffectiveDate(newEff);
if (newEff == null)
{
throw new ServiceException(MessageUtils.message("price.product.import.effEmpty"));
}
Date newEff = requireImportEffectiveDate(product.getEffectiveDate());
applyForcedEffectiveDate(product, countries, newEff);
assertImportEffectiveNotBeforePrev(product, newEff);
BmsProduct existing = productMapper.selectBmsProductByCodeAndDate(product.getProductCode(), newEff);
if (existing != null)
{
@@ -13,6 +13,7 @@ import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
@@ -21,6 +22,8 @@ import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.bms.ar.enums.ArIncomeBillFeeStatus;
import com.bms.ar.enums.ArOrderBillStatus;
import com.bms.common.annotation.DataScope;
import com.bms.common.core.domain.entity.SysUser;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.DictUtils;
import com.bms.common.utils.MessageUtils;
@@ -29,6 +32,7 @@ import com.bms.common.utils.StringUtils;
import com.bms.common.utils.sign.Md5Utils;
import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
import com.bms.system.mapper.SysUserMapper;
import com.bms.price.domain.PrcIncomeBill;
import com.bms.price.domain.PrcIncomeBillFee;
import com.bms.price.domain.PrcOrderBill;
@@ -70,6 +74,10 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
private static final String DICT_FEE_TYPE = "bms_fee_type";
private static final String FEE_TYPE_REFUND = "REFUND";
@Autowired
@Lazy
private IPrcOrderBillService self;
@Autowired
private ITmsPriceQuoteService tmsPriceQuoteService;
@@ -80,6 +88,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
@Autowired
private BmsCustomerMapper bmsCustomerMapper;
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private PrcOrderBillExtMapper orderBillExtMapper;
@@ -236,7 +247,12 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
}
}
/**
* 计费列表:按销售员/结算员字段做数据权限(仅本人/本部门等)。
* 编码口径同客户:belong_saler_code / settlement_man_code = sys_user.user_name(工号)或 user_id。
*/
@Override
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id", permission = "price:billing:bill")
public List<PrcOrderBill> selectBillList(PrcOrderBill query)
{
query.setOrderNos(normalizeCodeList(query.getOrderNos()));
@@ -248,33 +264,39 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
}
@Override
public List<String> listOrderNoOptions(String keyword)
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id", permission = "price:billing:bill")
public List<String> listOrderNoOptions(PrcOrderBill query)
{
if (StringUtils.isBlank(keyword))
if (query == null || query.getParams() == null
|| StringUtils.isBlank((String) query.getParams().get("optionKeyword")))
{
return new ArrayList<>();
}
return orderBillMapper.selectOrderNoOptions(keyword.trim(), 20);
return orderBillMapper.selectOrderNoOptions(query, 20);
}
@Override
public List<String> listTrackingNoOptions(String keyword)
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id", permission = "price:billing:bill")
public List<String> listTrackingNoOptions(PrcOrderBill query)
{
if (StringUtils.isBlank(keyword))
if (query == null || query.getParams() == null
|| StringUtils.isBlank((String) query.getParams().get("optionKeyword")))
{
return new ArrayList<>();
}
return orderBillMapper.selectTrackingNoOptions(keyword.trim(), 20);
return orderBillMapper.selectTrackingNoOptions(query, 20);
}
@Override
public List<String> listTransferNoOptions(String keyword)
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id", permission = "price:billing:bill")
public List<String> listTransferNoOptions(PrcOrderBill query)
{
if (StringUtils.isBlank(keyword))
if (query == null || query.getParams() == null
|| StringUtils.isBlank((String) query.getParams().get("optionKeyword")))
{
return new ArrayList<>();
}
return orderBillMapper.selectTransferNoOptions(keyword.trim(), 20);
return orderBillMapper.selectTransferNoOptions(query, 20);
}
/**
@@ -303,13 +325,17 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{
throw new ServiceException(MessageUtils.message("price.bill.billIdRequired"));
}
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(billId);
if (bill == null)
PrcOrderBill query = new PrcOrderBill();
query.setBillId(billId.trim());
// 走 selectBillList,复用销售员/结算员数据权限,避免越权查详情
List<PrcOrderBill> list = self.selectBillList(query);
if (list == null || list.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.bill.notFound"));
}
return toResult(bill, incomeBillFeeMapper.selectByBillId(billId),
payBillFeeMapper.selectByBillId(billId));
PrcOrderBill bill = list.get(0);
return toResult(bill, incomeBillFeeMapper.selectByBillId(bill.getBillId()),
payBillFeeMapper.selectByBillId(bill.getBillId()));
}
@Override
@@ -774,6 +800,45 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
return StringUtils.isEmpty(normalized) ? null : normalized.toUpperCase();
}
/**
* 同步订单:Oracle 销售员/结算员/客服为中文名,按 sys_user.nick_name 反查。
* 名称写 nick_name,编码写 user_name(工号,供数据权限);落到 prc_order_bill / prc_income_bill。
*/
private void resolveOrderStaffFromSysUser(TmsPriceBillRequest request)
{
if (request == null)
{
return;
}
SysUser saler = findUserByChineseName(request.getBelongSaler());
if (saler != null)
{
request.setBelongSaler(saler.getNickName());
request.setBelongSalerCode(saler.getUserName());
}
SysUser settlement = findUserByChineseName(request.getSettlementMan());
if (settlement != null)
{
request.setSettlementMan(settlement.getNickName());
request.setSettlementManCode(settlement.getUserName());
}
SysUser service = findUserByChineseName(request.getServiceMan());
if (service != null)
{
request.setServiceMan(service.getNickName());
request.setServiceManCode(service.getUserName());
}
}
private SysUser findUserByChineseName(String chineseName)
{
if (StringUtils.isBlank(chineseName))
{
return null;
}
return sysUserMapper.selectUserByNickName(chineseName.trim());
}
private void persistBill(TmsPriceBillRequest request, BigDecimal weight, Date bizDate,
TmsPriceQuoteResult result)
{
@@ -804,11 +869,14 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
bill.setCustomerName(request.getCustomerName());
bill.setTrackingNo(request.getTrackingNo());
bill.setTransferNo(request.getTransferNo());
// Oracle 中文人员名 -> sys_user.nick_name,*_code 写 user_name(数据权限)
resolveOrderStaffFromSysUser(request);
bill.setBelongSaler(request.getBelongSaler());
bill.setBelongSalerCode(request.getBelongSalerCode());
bill.setSettlementMan(request.getSettlementMan());
bill.setSettlementManCode(request.getSettlementManCode());
bill.setServiceMan(request.getServiceMan());
bill.setServiceManCode(request.getServiceManCode());
String customerActiveStatus = result == null ? null : result.getCustomerActiveStatus();
if (StringUtils.isEmpty(customerActiveStatus))
{
@@ -1109,6 +1177,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
fee.setFeeName(line.getFeeName());
fee.setScId(line.getScId() == null ? 0L : line.getScId());
fee.setAmount(line.getAmount() == null ? BigDecimal.ZERO : line.getAmount());
fee.setProductAmount(line.getProductAmount());
fee.setRedlineAmount(line.getRedlineAmount());
fee.setCurrency(line.getCurrency());
fee.setRoundMode(line.getRoundMode());
fee.setRemark(line.getRemark());
@@ -1301,6 +1371,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
line.setFeeName(fee.getFeeName());
line.setScId(fee.getScId());
line.setAmount(fee.getAmount());
line.setProductAmount(fee.getProductAmount());
line.setRedlineAmount(fee.getRedlineAmount());
line.setCurrency(fee.getCurrency());
line.setRoundMode(fee.getRoundMode());
line.setRemark(fee.getRemark());
@@ -1342,6 +1414,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
r.setSettlementMan(bill.getSettlementMan());
r.setSettlementManCode(bill.getSettlementManCode());
r.setServiceMan(bill.getServiceMan());
r.setServiceManCode(bill.getServiceManCode());
r.setCustomerName(bill.getCustomerName());
r.setCustomerActiveStatus(bill.getCustomerActiveStatus());
r.setCreditArrearsStatus(bill.getCreditArrearsStatus());
@@ -374,6 +374,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
throw new ServiceException(MessageUtils.message("price.set.edit.stopped"));
}
Date prevEffective = findPrevVersionEffectiveTime(version);
if (prevEffective != null && effectiveTime.before(prevEffective))
{
throw new ServiceException(MessageUtils.message("price.set.version.effectiveTime.beforePrev"));
}
// Set schedule; refreshVersionStatus decides PENDING(3) vs EFFECT(1) and chains expire_time
version.setEffectiveTime(effectiveTime);
version.setExpireTime(null);
@@ -383,6 +388,45 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
return 1;
}
/** Previous version (lower version_seq) effective_time; null if none. */
private Date findPrevVersionEffectiveTime(PrcPriceSetVersion version)
{
if (version == null || version.getPriceSetId() == null)
{
return null;
}
int curSeq = version.getVersionSeq() == null ? 0 : version.getVersionSeq().intValue();
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceSetId(version.getPriceSetId());
List<PrcPriceSetVersion> list = versionMapper.selectPrcPriceSetVersionList(q);
if (list == null || list.isEmpty())
{
return null;
}
PrcPriceSetVersion prev = null;
for (PrcPriceSetVersion v : list)
{
if (v == null || v.getEffectiveTime() == null)
{
continue;
}
if (version.getPsvId() != null && version.getPsvId().equals(v.getPsvId()))
{
continue;
}
int vs = v.getVersionSeq() == null ? 0 : v.getVersionSeq().intValue();
if (vs >= curSeq)
{
continue;
}
if (prev == null || vs > (prev.getVersionSeq() == null ? 0 : prev.getVersionSeq().intValue()))
{
prev = v;
}
}
return prev == null ? null : prev.getEffectiveTime();
}
@Override
@Transactional
public Long copyVersion(Long sourcePsvId, String createBy)
@@ -430,6 +474,35 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
return nv.getPsvId();
}
@Override
@Transactional
public Long createVersion(Long priceSetId, String createBy)
{
PrcPriceSet set = requireSet(priceSetId);
if (STATUS_STOP.equals(set.getStatus()))
{
throw new ServiceException(MessageUtils.message("price.set.edit.stopped"));
}
if (hasDraftVersion(set.getPriceSetId()))
{
throw new ServiceException(MessageUtils.message("price.set.version.create.draftExists"));
}
Integer maxSeq = versionMapper.selectMaxVersionSeq(set.getPriceSetId());
int seq = maxSeq == null ? 1 : maxSeq.intValue() + 1;
PrcPriceSetVersion nv = new PrcPriceSetVersion();
nv.setPriceSetId(set.getPriceSetId());
nv.setVersionSeq(Integer.valueOf(seq));
nv.setVersionNo("V" + seq);
nv.setStatus(VER_DRAFT);
nv.setEffectiveTime(null);
nv.setExpireTime(null);
nv.setPriceBuildMode(null);
nv.setCreateBy(createBy);
versionMapper.insertPrcPriceSetVersion(nv);
refreshVersionStatus(set.getPriceSetId());
return nv.getPsvId();
}
@Override
@Transactional
public int deleteVersion(Long psvId, String updateBy)
@@ -957,8 +957,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
r.setPayTotal(productAmount);
String currency = normalizeCurrency(r.getCurrency());
List<TmsPriceQuoteFeeLine> payLines = new ArrayList<TmsPriceQuoteFeeLine>();
payLines.add(feeLine(FEE_BASIC, feeNameOf(FEE_BASIC), 0L, productFreight, currency, roundMode, null));
payLines.add(feeLine(FEE_REG, feeNameOf(FEE_REG), 0L, baseReg, currency, roundMode, null));
TmsPriceQuoteFeeLine payFreight = feeLine(FEE_BASIC, feeNameOf(FEE_BASIC), 0L, productFreight, currency, roundMode, null);
payFreight.setProductAmount(productFreight);
payLines.add(payFreight);
TmsPriceQuoteFeeLine payReg = feeLine(FEE_REG, feeNameOf(FEE_REG), 0L, baseReg.setScale(2, rm), currency, roundMode, null);
payReg.setProductAmount(baseReg.setScale(2, rm));
payLines.add(payReg);
r.setPayFeeDetails(payLines);
}
@@ -971,8 +975,14 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
List<PrcSurchargeRel> rels = loadMatchedSurcharges(result);
List<TmsPriceQuoteFeeLine> lines = new ArrayList<TmsPriceQuoteFeeLine>();
lines.add(feeLine(FEE_BASIC, feeNameOf(FEE_BASIC), 0L, freightAmount, currency, result.getRoundMode(), null));
lines.add(feeLine(FEE_REG, feeNameOf(FEE_REG), 0L, registrationFee, currency, result.getRoundMode(), null));
BigDecimal productFreight = productAmountOf(result.getPayFeeDetails(), FEE_BASIC);
BigDecimal productReg = productAmountOf(result.getPayFeeDetails(), FEE_REG);
TmsPriceQuoteFeeLine basicLine = feeLine(FEE_BASIC, feeNameOf(FEE_BASIC), 0L, freightAmount, currency, result.getRoundMode(), null);
basicLine.setProductAmount(productFreight);
lines.add(basicLine);
TmsPriceQuoteFeeLine regLine = feeLine(FEE_REG, feeNameOf(FEE_REG), 0L, registrationFee, currency, result.getRoundMode(), null);
regLine.setProductAmount(productReg);
lines.add(regLine);
BigDecimal surchargeTotal = BigDecimal.ZERO;
Map<Long, TmsPriceQuoteFeeLine> surchargeLines = new LinkedHashMap<Long, TmsPriceQuoteFeeLine>();
@@ -1382,6 +1392,23 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
return Md5Utils.hash(raw);
}
/** Read catalog product amount for a fee type from payFeeDetails. */
private static BigDecimal productAmountOf(List<TmsPriceQuoteFeeLine> payLines, String feeType)
{
if (payLines == null || feeType == null)
{
return null;
}
for (TmsPriceQuoteFeeLine line : payLines)
{
if (line != null && feeType.equals(line.getFeeType()))
{
return line.getProductAmount() != null ? line.getProductAmount() : line.getAmount();
}
}
return null;
}
private static TmsPriceQuoteFeeLine feeLine(String feeType, String feeName, Long scId, BigDecimal amount,
String currency, String roundMode, String remark)
{
@@ -1,5 +1,6 @@
package com.bms.price.task;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -30,12 +31,14 @@ public class BmsProductImportAsyncRunner
/** Background all-country (Weishi catalog) import. */
@Async("threadPoolTaskExecutor")
public void runImportAll(String taskId, byte[] bytes, String filename, boolean updateSupport, String operName)
public void runImportAll(String taskId, byte[] bytes, String filename, boolean updateSupport, String operName,
Date effectiveDate)
{
try
{
put(taskId, BmsProductImportTask.STATUS_RUNNING, 5, "parsing excel", null);
String message = productService.importProductExcel(bytes, filename, updateSupport, operName, taskId);
String message = productService.importProductExcel(bytes, filename, updateSupport, operName, taskId,
effectiveDate);
put(taskId, BmsProductImportTask.STATUS_SUCCESS, 100, message, null);
log.info("async product import done, taskId={}", taskId);
}
@@ -48,12 +51,14 @@ public class BmsProductImportAsyncRunner
/** Background single-country cost-sheet import. */
@Async("threadPoolTaskExecutor")
public void runImportCountry(String taskId, byte[] bytes, String filename, String countryCode, String operName)
public void runImportCountry(String taskId, byte[] bytes, String filename, String countryCode, String operName,
Date effectiveDate)
{
try
{
put(taskId, BmsProductImportTask.STATUS_RUNNING, 5, "parsing excel", null);
String message = productService.importCountryProductExcel(bytes, filename, countryCode, operName, taskId);
String message = productService.importCountryProductExcel(bytes, filename, countryCode, operName, taskId,
effectiveDate);
put(taskId, BmsProductImportTask.STATUS_SUCCESS, 100, message, null);
log.info("async product country import done, taskId={}, country={}", taskId, countryCode);
}
@@ -51,6 +51,14 @@ public interface SysUserMapper
*/
public SysUser selectUserByUserName(String userName);
/**
* 通过中文昵称(nick_name)精确查用户(未删除,优先启用)
*
* @param nickName 中文名称
* @return 用户对象信息
*/
public SysUser selectUserByNickName(String nickName);
/**
* 通过用户ID查询用户
*
@@ -191,8 +191,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<update id="refreshStatusByDates">
update product_country
set status = case
when expire_date is not null and expire_date &lt;= curdate() then '2'
when effective_date is null or effective_date &gt; curdate() then '0'
when expire_date is not null and expire_date &lt;= now() then '2'
when effective_date is null or effective_date &gt; now() then '0'
else '1'
end
</update>
@@ -206,8 +206,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<update id="refreshStatusByDates">
update product
set status = case
when expire_date is not null and expire_date &lt;= curdate() then '2'
when effective_date is null or effective_date &gt; curdate() then '0'
when expire_date is not null and expire_date &lt;= now() then '2'
when effective_date is null or effective_date &gt; now() then '0'
else '1'
end
</update>
@@ -12,6 +12,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="feeName" column="fee_name" />
<result property="scId" column="sc_id" />
<result property="amount" column="amount" />
<result property="productAmount" column="product_amount" />
<result property="redlineAmount" column="redline_amount" />
<result property="currency" column="currency" />
<result property="roundMode" column="round_mode" />
<result property="remark" column="remark" />
@@ -21,7 +23,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<select id="selectByBillId" parameterType="String" resultMap="PrcIncomeBillFeeResult">
select fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
select fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, product_amount, redline_amount, currency, round_mode, remark,
ar_bill_status, manual_flag, create_time
from prc_income_bill_fee
where bill_id = #{billId}
@@ -29,7 +31,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<select id="selectByBillIds" resultMap="PrcIncomeBillFeeResult">
select fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
select fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, product_amount, redline_amount, currency, round_mode, remark,
ar_bill_status, manual_flag, create_time
from prc_income_bill_fee
where bill_id in
@@ -40,7 +42,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<select id="selectUnbilledByBillIds" resultMap="PrcIncomeBillFeeResult">
select fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
select fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, product_amount, redline_amount, currency, round_mode, remark,
ar_bill_status, manual_flag, create_time
from prc_income_bill_fee
where ar_bill_status = #{arBillStatus}
@@ -52,7 +54,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<select id="selectByFeeId" parameterType="String" resultMap="PrcIncomeBillFeeResult">
select fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
select fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, product_amount, redline_amount, currency, round_mode, remark,
ar_bill_status, manual_flag, create_time
from prc_income_bill_fee
where fee_id = #{feeId}
@@ -68,21 +70,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<insert id="insertPrcIncomeBillFee" parameterType="PrcIncomeBillFee">
insert into prc_income_bill_fee (
fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, product_amount, redline_amount, currency, round_mode, remark,
ar_bill_status, manual_flag, create_time
) values (
#{feeId}, #{billId}, #{incomeBillId}, #{feeType}, #{feeName}, #{scId}, #{amount}, #{currency}, #{roundMode}, #{remark},
#{feeId}, #{billId}, #{incomeBillId}, #{feeType}, #{feeName}, #{scId}, #{amount}, #{productAmount}, #{redlineAmount}, #{currency}, #{roundMode}, #{remark},
#{arBillStatus}, #{manualFlag}, sysdate()
)
</insert>
<insert id="insertBatch" parameterType="java.util.List">
insert into prc_income_bill_fee (
fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark,
fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, product_amount, redline_amount, currency, round_mode, remark,
ar_bill_status, manual_flag, create_time
) values
<foreach collection="list" item="item" separator=",">
(#{item.feeId}, #{item.billId}, #{item.incomeBillId}, #{item.feeType}, #{item.feeName}, #{item.scId}, #{item.amount},
#{item.productAmount}, #{item.redlineAmount},
#{item.currency}, #{item.roundMode}, #{item.remark},
#{item.arBillStatus}, #{item.manualFlag}, sysdate())
</foreach>
@@ -94,6 +97,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
fee_name = #{feeName},
sc_id = #{scId},
amount = #{amount},
product_amount = #{productAmount},
redline_amount = #{redlineAmount},
currency = #{currency},
round_mode = #{roundMode},
remark = #{remark}
@@ -17,6 +17,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="settlementMan" column="settlement_man" />
<result property="settlementManCode" column="settlement_man_code" />
<result property="serviceMan" column="service_man" />
<result property="serviceManCode" column="service_man_code" />
<result property="customerId" column="customer_id" />
<result property="customerCode" column="customer_code" />
<result property="customerName" column="customer_name" />
@@ -77,7 +78,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="selectPrcOrderBillVo">
select bill_id, bill_key, order_no, tracking_no, transfer_no,
belong_saler, belong_saler_code, settlement_man, settlement_man_code, service_man,
belong_saler, belong_saler_code, settlement_man, settlement_man_code, service_man, service_man_code,
customer_id, customer_code, customer_name,
pieces, actual_weight, volume, volume_weight, pkg_length, pkg_width, pkg_height,
shipper_name, consignee_name, dest_postcode, warehouse_code, order_time, order_remark,
@@ -89,13 +90,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
freight_amount, surcharge_amount, income_total, pay_total, currency, ar_bill_status,
customer_active_status, credit_arrears_status, round_mode,
created_by, create_time, updated_by, update_time
from prc_order_bill
from prc_order_bill p
</sql>
<!-- 应收生成专用来源快照:income_total 只汇总当前仍未出账的费用。 -->
<sql id="selectArBillSourceVo">
select p.bill_id, p.bill_key, p.order_no, p.tracking_no, p.transfer_no,
p.belong_saler, p.belong_saler_code, p.settlement_man, p.settlement_man_code, p.service_man,
p.belong_saler, p.belong_saler_code, p.settlement_man, p.settlement_man_code, p.service_man, p.service_man_code,
p.customer_id, p.customer_code, p.customer_name,
p.pieces, p.actual_weight, p.volume, p.volume_weight, p.pkg_length, p.pkg_width, p.pkg_height,
p.shipper_name, p.consignee_name, p.dest_postcode, p.warehouse_code, p.order_time, p.order_remark,
@@ -150,119 +151,167 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where bill_id = #{billId}
</select>
<!-- 计费列表数据权限:当前用户为销售员或结算员(工号=user_name 或 user_id)可见;
@DataScope 注入 su.user_id / su.dept_id 条件;空则不过滤(管理员/全部数据权限) -->
<sql id="orderBillDataScopeFilter">
<if test="params != null and params.dataScope != null and params.dataScope != ''">
AND EXISTS (
SELECT 1 FROM sys_user su
WHERE su.del_flag = '0'
AND (
(p.belong_saler_code IS NOT NULL AND p.belong_saler_code != ''
AND (su.user_name = p.belong_saler_code COLLATE utf8mb4_general_ci
OR su.user_id = p.belong_saler_code + 0))
OR
(p.settlement_man_code IS NOT NULL AND p.settlement_man_code != ''
AND (su.user_name = p.settlement_man_code COLLATE utf8mb4_general_ci
OR su.user_id = p.settlement_man_code + 0))
)
${params.dataScope}
)
</if>
</sql>
<!-- options query: query + limit, dataScope in query.params -->
<sql id="orderBillDataScopeFilterByQuery">
<if test="query != null and query.params != null and query.params.dataScope != null and query.params.dataScope != ''">
AND EXISTS (
SELECT 1 FROM sys_user su
WHERE su.del_flag = '0'
AND (
(p.belong_saler_code IS NOT NULL AND p.belong_saler_code != ''
AND (su.user_name = p.belong_saler_code COLLATE utf8mb4_general_ci
OR su.user_id = p.belong_saler_code + 0))
OR
(p.settlement_man_code IS NOT NULL AND p.settlement_man_code != ''
AND (su.user_name = p.settlement_man_code COLLATE utf8mb4_general_ci
OR su.user_id = p.settlement_man_code + 0))
)
${query.params.dataScope}
)
</if>
</sql>
<select id="selectOrderNoOptions" resultType="String">
select order_no
from prc_order_bill
select p.order_no
from prc_order_bill p
<where>
<if test="keyword != null and keyword != ''">
order_no like concat('%', #{keyword}, '%')
<if test="query.params != null and query.params.optionKeyword != null and query.params.optionKeyword != ''">
AND p.order_no like concat('%', #{query.params.optionKeyword}, '%')
</if>
<include refid="orderBillDataScopeFilterByQuery"/>
</where>
group by order_no
order by max(create_time) desc
group by p.order_no
order by max(p.create_time) desc
limit #{limit}
</select>
<select id="selectTrackingNoOptions" resultType="String">
select tracking_no
from prc_order_bill
select p.tracking_no
from prc_order_bill p
<where>
<if test="keyword != null and keyword != ''">
tracking_no like concat('%', #{keyword}, '%')
<if test="query.params != null and query.params.optionKeyword != null and query.params.optionKeyword != ''">
AND p.tracking_no like concat('%', #{query.params.optionKeyword}, '%')
</if>
<include refid="orderBillDataScopeFilterByQuery"/>
</where>
group by tracking_no
order by max(create_time) desc
group by p.tracking_no
order by max(p.create_time) desc
limit #{limit}
</select>
<select id="selectTransferNoOptions" resultType="String">
select transfer_no
from prc_order_bill
select p.transfer_no
from prc_order_bill p
<where>
<if test="keyword != null and keyword != ''">
transfer_no like concat('%', #{keyword}, '%')
<if test="query.params != null and query.params.optionKeyword != null and query.params.optionKeyword != ''">
AND p.transfer_no like concat('%', #{query.params.optionKeyword}, '%')
</if>
<include refid="orderBillDataScopeFilterByQuery"/>
</where>
group by transfer_no
order by max(create_time) desc
group by p.transfer_no
order by max(p.create_time) desc
limit #{limit}
</select>
<select id="selectPrcOrderBillList" parameterType="PrcOrderBill" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
<where>
<if test="billId != null and billId != ''">
AND p.bill_id = #{billId}
</if>
<if test="orderNo != null and orderNo != ''">
AND order_no like concat('%', #{orderNo}, '%')
AND p.order_no like concat('%', #{orderNo}, '%')
</if>
<if test="orderNos != null and orderNos.size() > 0">
AND order_no IN
AND p.order_no IN
<foreach collection="orderNos" item="ono" open="(" separator="," close=")">
#{ono}
</foreach>
</if>
<if test="trackingNo != null and trackingNo != ''">
AND tracking_no like concat('%', #{trackingNo}, '%')
AND p.tracking_no like concat('%', #{trackingNo}, '%')
</if>
<if test="trackingNos != null and trackingNos.size() > 0">
AND tracking_no IN
AND p.tracking_no IN
<foreach collection="trackingNos" item="tno" open="(" separator="," close=")">
#{tno}
</foreach>
</if>
<if test="transferNo != null and transferNo != ''">
AND transfer_no like concat('%', #{transferNo}, '%')
AND p.transfer_no like concat('%', #{transferNo}, '%')
</if>
<if test="transferNos != null and transferNos.size() > 0">
AND transfer_no IN
AND p.transfer_no IN
<foreach collection="transferNos" item="rno" open="(" separator="," close=")">
#{rno}
</foreach>
</if>
<if test="customerActiveStatus != null and customerActiveStatus != ''">
AND customer_active_status = #{customerActiveStatus}
AND p.customer_active_status = #{customerActiveStatus}
</if>
<if test="creditArrearsStatus != null and creditArrearsStatus != ''">
AND credit_arrears_status = #{creditArrearsStatus}
AND p.credit_arrears_status = #{creditArrearsStatus}
</if>
<if test="customerCode != null and customerCode != ''">
AND customer_code like concat('%', #{customerCode}, '%')
AND p.customer_code like concat('%', #{customerCode}, '%')
</if>
<if test="customerCodes != null and customerCodes.size() > 0">
AND customer_code IN
AND p.customer_code IN
<foreach collection="customerCodes" item="ccode" open="(" separator="," close=")">
#{ccode}
</foreach>
</if>
<if test="customerName != null and customerName != ''">
AND customer_name like concat('%', #{customerName}, '%')
AND p.customer_name like concat('%', #{customerName}, '%')
</if>
<if test="productCode != null and productCode != ''">
AND product_code like concat('%', #{productCode}, '%')
AND p.product_code like concat('%', #{productCode}, '%')
</if>
<if test="productCodes != null and productCodes.size() > 0">
AND product_code IN
AND p.product_code IN
<foreach collection="productCodes" item="pcode" open="(" separator="," close=")">
#{pcode}
</foreach>
</if>
<if test="countryCode != null and countryCode != ''">
AND country_code = #{countryCode}
AND p.country_code = #{countryCode}
</if>
<if test="matchSource != null and matchSource != ''">
AND match_source = #{matchSource}
AND p.match_source = #{matchSource}
</if>
<if test="bizDate != null">
AND biz_date = #{bizDate}
AND p.biz_date = #{bizDate}
</if>
<if test="params.beginTime != null and params.beginTime != ''">
AND date_format(create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
AND date_format(p.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''">
AND date_format(create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
AND date_format(p.create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
</if>
<include refid="orderBillDataScopeFilter"/>
</where>
order by create_time desc, bill_id desc
order by p.create_time desc, p.bill_id desc
</select>
<!-- rename_bill_tables.sql 已将需求文档中的 prc_bill 映射为本项目的 prc_order_bill。 -->
@@ -449,7 +498,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<insert id="insertPrcOrderBill" parameterType="PrcOrderBill">
insert into prc_order_bill (
bill_id, bill_key, order_no, tracking_no, transfer_no,
belong_saler, belong_saler_code, settlement_man, settlement_man_code, service_man,
belong_saler, belong_saler_code, settlement_man, settlement_man_code, service_man, service_man_code,
customer_id, customer_code, customer_name,
pieces, actual_weight, volume, volume_weight, pkg_length, pkg_width, pkg_height,
shipper_name, consignee_name, dest_postcode, warehouse_code, order_time, order_remark,
@@ -463,7 +512,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
create_time
) values (
#{billId}, #{billKey}, #{orderNo}, #{trackingNo}, #{transferNo},
#{belongSaler}, #{belongSalerCode}, #{settlementMan}, #{settlementManCode}, #{serviceMan},
#{belongSaler}, #{belongSalerCode}, #{settlementMan}, #{settlementManCode}, #{serviceMan}, #{serviceManCode},
#{customerId}, #{customerCode}, #{customerName},
#{pieces}, #{actualWeight}, #{volume}, #{volumeWeight}, #{pkgLength}, #{pkgWidth}, #{pkgHeight},
#{shipperName}, #{consigneeName}, #{destPostcode}, #{warehouseCode}, #{orderTime}, #{orderRemark},
@@ -488,6 +537,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
settlement_man = #{settlementMan},
settlement_man_code = #{settlementManCode},
service_man = #{serviceMan},
service_man_code = #{serviceManCode},
customer_id = #{customerId},
customer_code = #{customerCode},
customer_name = #{customerName},
@@ -137,6 +137,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<include refid="selectUserVo"/>
where u.user_name = #{userName} and u.del_flag = '0'
</select>
<select id="selectUserByNickName" parameterType="String" resultMap="SysUserResult">
<include refid="selectUserVo"/>
where u.nick_name = #{nickName} and u.del_flag = '0'
order by case when u.status = '0' then 0 else 1 end, u.user_id
limit 1
</select>
<select id="selectUserById" parameterType="Long" resultMap="SysUserResult">
<include refid="selectUserVo"/>
+7 -4
View File
@@ -292,8 +292,8 @@ CREATE TABLE `product` (
`dest_region` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目的区域:欧盟等',
`update_note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新内容说明',
`tax_mode` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'TAX_INCLUDED' COMMENT 'TAX_INCLUDED包税 TAX_EXCLUDED不包税',
`effective_date` date DEFAULT NULL COMMENT '目录生效日期',
`expire_date` date DEFAULT NULL COMMENT '产品失效日期(空=长期;下一版生效日=本版失效日)',
`effective_date` datetime DEFAULT NULL COMMENT '目录生效时间(到小时)',
`expire_date` datetime DEFAULT NULL COMMENT '产品失效时间(空=长期;下一版生效时间=本版失效时间)',
`status` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '0待生效 1已生效 2已失效',
`source_sheet` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '来源Excel sheet名',
`remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
@@ -329,8 +329,8 @@ CREATE TABLE `product_country` (
`size_limit` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '尺寸限制',
`last_mile` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '尾程派送说明',
`lead_time` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '参考时效',
`effective_date` date DEFAULT NULL COMMENT '产品价表生效日期',
`expire_date` date DEFAULT NULL COMMENT 'expire date (null=open; set to next import effective_date)',
`effective_date` datetime DEFAULT NULL COMMENT '产品价表生效时间(到小时)',
`expire_date` datetime DEFAULT NULL COMMENT '失效时间(空=开放;下一版导入生效时间=本版失效时间)',
`status` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '0待生效 1已生效 2已失效',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
@@ -896,6 +896,8 @@ CREATE TABLE `prc_income_bill_fee` (
`fee_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '费用名称',
`sc_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '附加费配置ID',
`amount` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '金额',
`product_amount` decimal(18,4) DEFAULT NULL COMMENT '产品价',
`redline_amount` decimal(18,4) DEFAULT NULL COMMENT '红线价',
`currency` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'CNY' COMMENT '币种',
`round_mode` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT 'HALF_UP' COMMENT '取值方式',
`remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
@@ -925,6 +927,7 @@ CREATE TABLE `prc_order_bill` (
`settlement_man` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '结算员',
`settlement_man_code` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '结算员编码',
`service_man` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '客服员',
`service_man_code` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '客服员编码',
`customer_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '客户ID',
`customer_code` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '客户代码',
`customer_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '客户名称',
@@ -0,0 +1,15 @@
-- prc_income_bill_fee: product / redline amounts (run once on existing DB)
SET @col_exists := (
SELECT COUNT(1) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'prc_income_bill_fee' AND COLUMN_NAME = 'product_amount'
);
SET @sql := IF(@col_exists = 0,
CONCAT(
'ALTER TABLE `prc_income_bill_fee` ',
'ADD COLUMN `product_amount` decimal(18,4) DEFAULT NULL COMMENT ''产品价'' AFTER `amount`, ',
'ADD COLUMN `redline_amount` decimal(18,4) DEFAULT NULL COMMENT ''红线价'' AFTER `product_amount`'
),
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
@@ -0,0 +1,11 @@
-- prc_order_bill: service_man_code (run once on existing DB)
SET @col_exists := (
SELECT COUNT(1) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'prc_order_bill' AND COLUMN_NAME = 'service_man_code'
);
SET @sql := IF(@col_exists = 0,
'ALTER TABLE `prc_order_bill` ADD COLUMN `service_man_code` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ''客服员编码'' AFTER `service_man`',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
@@ -0,0 +1,8 @@
-- product / product_country: effective_date & expire_date support hour precision
ALTER TABLE product
MODIFY COLUMN effective_date datetime DEFAULT NULL COMMENT '目录生效时间(到小时)',
MODIFY COLUMN expire_date datetime DEFAULT NULL COMMENT '产品失效时间(空=长期;下一版生效时间=本版失效时间)';
ALTER TABLE product_country
MODIFY COLUMN effective_date datetime DEFAULT NULL COMMENT '产品价表生效时间(到小时)',
MODIFY COLUMN expire_date datetime DEFAULT NULL COMMENT '失效时间(空=开放;下一版导入生效时间=本版失效时间)';
+18 -3
View File
@@ -9,6 +9,15 @@ export function listProduct(query) {
})
}
/** 产品下拉选项(非分页,不要求 price:product:list 权限) */
export function listProductOptions(query) {
return request({
url: '/price/product/optionselect',
method: 'get',
params: query
})
}
/** 查询产品详情 */
export function getProduct(productId) {
return request({
@@ -111,13 +120,17 @@ async function pollProductImportTask(taskId, onProgress) {
/**
* Async all-country product import; poll until done.
* @param {File|Blob} file
* @param {string} effectiveDate yyyy-MM-dd (required)
* @param {(task: object) => void} [onProgress]
*/
export async function importProductExcelAsync(file, onProgress) {
export async function importProductExcelAsync(file, effectiveDate, onProgress) {
const formData = new FormData()
formData.append('file', file)
const params = new URLSearchParams()
params.set('updateSupport', '0')
if (effectiveDate) params.set('effectiveDate', effectiveDate)
const submitRes = await request({
url: '/price/product/importData?updateSupport=0',
url: '/price/product/importData?' + params.toString(),
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
@@ -133,13 +146,15 @@ export async function importProductExcelAsync(file, onProgress) {
* Async single-country product import; poll until done.
* @param {File|Blob} file
* @param {string} countryCode
* @param {string} effectiveDate yyyy-MM-dd
* @param {(task: object) => void} [onProgress]
*/
export async function importProductCountryExcelAsync(file, countryCode, onProgress) {
export async function importProductCountryExcelAsync(file, countryCode, effectiveDate, onProgress) {
const formData = new FormData()
formData.append('file', file)
const params = new URLSearchParams()
params.set('countryCode', countryCode)
if (effectiveDate) params.set('effectiveDate', effectiveDate)
const submitRes = await request({
url: '/price/product/importCountryData?' + params.toString(),
method: 'post',
+8
View File
@@ -128,6 +128,14 @@ export function copyPriceSetVersion(psvId) {
})
}
/** Create empty draft version under package */
export function createPriceSetVersion(priceSetId) {
return request({
url: '/price/set/version/create/' + priceSetId,
method: 'post'
})
}
export function deletePriceSetVersion(psvId) {
return request({
url: '/price/set/version/' + psvId,
@@ -1,5 +1,5 @@
import { ref } from 'vue'
import { listProduct } from '@/api/price/product'
import { listProductOptions } from '@/api/price/product'
/**
* 产品列表模块级共享缓存:全应用只拉取一次
@@ -37,9 +37,9 @@ export function useProducts() {
function ensureProducts(force = false) {
if (!force && loadingPromise) return loadingPromise
if (!force && productList.value.length) return Promise.resolve(productList.value)
loadingPromise = listProduct({ pageNum: 1, pageSize: 1000 })
loadingPromise = listProductOptions({})
.then(res => {
productList.value = res.rows || []
productList.value = res.data || []
return productList.value
})
.finally(() => {
@@ -63,13 +63,13 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="生效日期" prop="effectiveDate">
<el-date-picker v-model="editForm.effectiveDate" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
<el-form-item label="生效时间" prop="effectiveDate">
<el-date-picker v-model="editForm.effectiveDate" type="datetime" format="YYYY-MM-DD HH:00" value-format="YYYY-MM-DD HH:00:00" placeholder="选择时间" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="失效日期" prop="expireDate">
<el-date-picker v-model="editForm.expireDate" type="date" value-format="YYYY-MM-DD" placeholder="留空为长期" style="width: 100%" />
<el-form-item label="失效时间" prop="expireDate">
<el-date-picker v-model="editForm.expireDate" type="datetime" format="YYYY-MM-DD HH:00" value-format="YYYY-MM-DD HH:00:00" placeholder="留空为长期" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24">
@@ -112,8 +112,8 @@ const columns = [
{ prop: 'sizeLimit', label: '尺寸限制', minWidth: 140 },
{ prop: 'lastMile', label: '尾程', minWidth: 110 },
{ prop: 'leadTime', label: '时效', width: 100, showOverflowTooltip: true },
{ prop: 'effectiveDate', label: '生效日期', width: 110, align: 'center' },
{ prop: 'expireDate', label: '失效日期', width: 110, align: 'center', formatter: (row) => row.expireDate || '长期' },
{ prop: 'effectiveDate', label: '生效时间', width: 160, align: 'center' },
{ prop: 'expireDate', label: '失效时间', width: 110, align: 'center', formatter: (row) => row.expireDate || '长期' },
{ prop: 'status', label: '状态', width: 90, align: 'center', dict: 'bms_product_effect_status' },
// { prop: 'action', label: '操作', width: 70, fixed: 'right', slot: 'action' }
]
@@ -176,6 +176,15 @@ const editTitle = ref('编辑国家明细')
const editFormRef = ref()
const saving = ref(false)
const editRow = ref({})
function normalizeDrawerHour(val) {
if (val == null || val === '') return ''
const s = String(val).trim().replace('T', ' ')
const m = s.match(/^(\d{4}-\d{2}-\d{2})[ ](\d{2})/)
if (m) return m[1] + ' ' + m[2] + ':00:00'
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s + ' 00:00:00'
return s
}
const editForm = reactive({
detailId: null,
weightMin: null,
@@ -202,7 +211,7 @@ const editRules = {
trigger: 'blur'
}],
freightPrice: [{ required: true, message: '运费不能为空', trigger: 'blur' }],
effectiveDate: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
effectiveDate: [{ required: true, message: '生效时间不能为空', trigger: 'change' }]
}
function openEdit(row) {
@@ -217,8 +226,8 @@ function openEdit(row) {
sizeLimit: row.sizeLimit || '',
lastMile: row.lastMile || '',
leadTime: row.leadTime || '',
effectiveDate: row.effectiveDate ? String(row.effectiveDate).slice(0, 10) : '',
expireDate: row.expireDate ? String(row.expireDate).slice(0, 10) : ''
effectiveDate: row.effectiveDate ? normalizeDrawerHour(row.effectiveDate) : '',
expireDate: row.expireDate ? normalizeDrawerHour(row.expireDate) : '',
})
editVisible.value = true
nextTick(() => editFormRef.value?.clearValidate())
@@ -80,19 +80,19 @@ const schemas = computed(() => [
// rules: [{ required: true, message: '请选择税模式', trigger: 'change' }]
// },
{
label: '生效日期',
label: '生效时间',
prop: 'effectiveDate',
component: 'date',
placeholder: '选择日期',
component: 'datetime',
placeholder: '选择时间',
span: 12,
width: '100%',
rules: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
rules: [{ required: true, message: '生效时间不能为空', trigger: 'change' }]
},
{
label: '失效日期',
label: '失效时间',
prop: 'expireDate',
component: 'date',
placeholder: '选择日期',
component: 'datetime',
placeholder: '选择时间',
span: 12,
width: '100%'
},
@@ -5,7 +5,7 @@
:closable="false"
show-icon
class="mb16"
title="多国家价表用「全部导入」。单国专线成本价用「按国家导入」:先选目的国家,目录里的产品都记到该国。相同产品代码+生效日期不覆盖主数据。两种导入均可在弹窗内下载对应模板。"
title="导入前须先选择生效时间;产品与国家明细均按此时间落库,上一版产品/明细失效时间设为该时间。多国家价表用「全部导入」,单国专线成本价用「按国家导入」。相同产品代码+生效日期不覆盖主数据。"
/>
<basic-table
@@ -65,9 +65,27 @@
template-url="/templates/product/product-import-all-template.xlsx"
template-file-name="产品多国家价表导入模板"
:import-desc="allImportDesc"
:before-submit="validateImportEffectiveDate"
:submit-handler="runImportAllAsync"
@success="() => tableRef.refresh()"
/>
>
<template #extra>
<el-form label-width="90px">
<el-form-item label="生效时间" required>
<el-date-picker
v-model="importEffectiveDate"
type="datetime"
format="YYYY-MM-DD HH:00"
value-format="YYYY-MM-DD HH:00:00"
popper-class="product-import-hour-picker"
placeholder="请选择生效时间"
style="width: 100%"
@change="onImportEffectiveTimeChange"
/>
</el-form-item>
</el-form>
</template>
</excel-import-dialog>
<excel-import-dialog
ref="countryImportRef"
title="按国家导入"
@@ -82,6 +100,18 @@
>
<template #extra>
<el-form label-width="90px">
<el-form-item label="生效时间" required>
<el-date-picker
v-model="importEffectiveDate"
type="datetime"
format="YYYY-MM-DD HH:00"
value-format="YYYY-MM-DD HH:00:00"
popper-class="product-import-hour-picker"
placeholder="请选择生效时间"
style="width: 100%"
@change="onImportEffectiveTimeChange"
/>
</el-form-item>
<el-form-item label="目的国家" required>
<el-select v-model="importCountryCode" filterable clearable placeholder="请选择国家" style="width: 100%">
<el-option
@@ -133,6 +163,31 @@ const expiredRef = ref()
const importRef = ref()
const countryImportRef = ref()
const importCountryCode = ref('')
const importEffectiveDate = ref('')
function pad2(n) {
return String(n).padStart(2, '0')
}
function formatImportHour(d) {
if (!d || Number.isNaN(d.getTime())) return undefined
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
+ ' ' + pad2(d.getHours()) + ':00:00'
}
function normalizeImportToHour(val) {
if (val == null || val === '') return val
if (val instanceof Date) return formatImportHour(val)
const s = String(val).trim().replace('T', ' ')
const m = s.match(/^(\d{4}-\d{2}-\d{2})[ ](\d{2})/)
if (m) return m[1] + ' ' + m[2] + ':00:00'
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s + ' 00:00:00'
return formatImportHour(new Date(s.replace(/-/g, '/'))) || val
}
function onImportEffectiveTimeChange(val) {
importEffectiveDate.value = normalizeImportToHour(val) || ''
}
const regionOptions = ref([])
const importProgressVisible = ref(false)
const importProgressPercent = ref(0)
@@ -140,12 +195,12 @@ const importProgressMessage = ref('')
const importProgressStatus = ref(undefined)
const allImportDesc = [
'适用多国家价表(B2C 小包价表等),按目录 sheet 匹配国家导入。',
'生效日期必填。产品+国家有新生效日时,先失效该国家下全部旧重量段,再插入 Excel 重量段。',
'必选生效时间:产品与 product_country 明细均按此时间计费;上一版产品/明细失效时间设为该时间。',
'请在弹窗底部下载模板,按模板格式整理后上传。相同产品代码+生效日期不覆盖主数据。'
]
const countryImportDesc = [
'选择目的国家后上传专线成本价表,目录中的产品都会记到该国家。',
'生效日期必填。产品+国家有新生效日时,先失效该国家下全部旧重量段,再插入 Excel 重量段。',
'必选生效时间:产品与 product_country 明细均按此时间计费;上一版产品/明细失效时间设为该时间。',
'邮编、赔偿标准等 sheet 不导入。可在弹窗底部下载模板。'
]
@@ -155,8 +210,8 @@ const columns = [
{ prop: 'serviceLevel', label: '时效等级', width: 90, align: 'center' },
{ prop: 'cargoAttr', label: '走货属性', minWidth: 120, slot: 'cargoAttr' },
{ prop: 'destRegion', label: '目的区域', width: 90, align: 'center' },
{ prop: 'effectiveDate', label: '生效日期', width: 120, align: 'center' },
{ prop: 'expireDate', label: '失效日期', width: 120, align: 'center' },
{ prop: 'effectiveDate', label: '生效时间', width: 160, align: 'center' },
{ prop: 'expireDate', label: '失效时间', width: 160, align: 'center' },
{ prop: 'countryCount', label: '国家数', width: 80, align: 'center' },
{ prop: 'surchargeCount', label: '附加费数', width: 90, align: 'center' },
{ prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' },
@@ -190,6 +245,7 @@ function handleImportCommand(command) {
}
function handleImport() {
importEffectiveDate.value = ''
importRef.value.open()
}
@@ -208,11 +264,21 @@ function loadRegionOptions() {
function handleCountryImport() {
importCountryCode.value = ''
importEffectiveDate.value = ''
loadRegionOptions()
countryImportRef.value.open()
}
function validateImportEffectiveDate() {
if (!importEffectiveDate.value) {
return '请先选择生效时间'
}
return ''
}
function validateCountryImport() {
const effErr = validateImportEffectiveDate()
if (effErr) return effErr
if (!importCountryCode.value) {
return '请先选择目的国家'
}
@@ -266,7 +332,11 @@ async function runImportAllAsync(uploadFile) {
if (!raw) {
throw new Error('请先选择文件')
}
return runImportWithProgress((onProgress) => importProductExcelAsync(raw, onProgress))
if (!importEffectiveDate.value) {
throw new Error('请先选择生效时间')
}
const eff = importEffectiveDate.value
return runImportWithProgress((onProgress) => importProductExcelAsync(raw, eff, onProgress))
}
async function runImportCountryAsync(uploadFile) {
@@ -274,11 +344,15 @@ async function runImportCountryAsync(uploadFile) {
if (!raw) {
throw new Error('请先选择文件')
}
if (!importEffectiveDate.value) {
throw new Error('请先选择生效时间')
}
if (!importCountryCode.value) {
throw new Error('请先选择目的国家')
}
const code = importCountryCode.value
return runImportWithProgress((onProgress) => importProductCountryExcelAsync(raw, code, onProgress))
const eff = importEffectiveDate.value
return runImportWithProgress((onProgress) => importProductCountryExcelAsync(raw, code, eff, onProgress))
}
</script>
@@ -24,7 +24,7 @@
</div>
</template>
<div v-if="!livePersist" class="build-choice">
<div v-if="showBuildChoice" class="build-choice">
<p class="build-choice__tip">创建价格方式</p>
<div class="build-choice__cards">
<button
@@ -224,6 +224,11 @@ const hasBuildMode = computed(() => {
// Legacy / already has products: skip choice cards
return (props.products || []).some(p => p && p.productCode)
})
/** Create/newVersion only: show choice until a mode is picked */
const showBuildChoice = computed(() => {
if (props.livePersist) return false
return !isPullBuildMode(props.priceBuildMode) && !isImportBuildMode(props.priceBuildMode)
})
const choosingMode = ref(false)
const isModifyImport = computed(() => {
if (isPullBuildMode(props.priceBuildMode)) return true
@@ -1,5 +1,15 @@
<template>
<set-section-card :title="title" tone="green" :count="filteredVersions.length">
<template #action>
<el-button
type="primary"
plain
size="small"
:disabled="hasDraft"
v-hasPermi="['price:set:add']"
@click="emit('create-version')"
>新建价格版本</el-button>
</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" />
@@ -80,7 +90,7 @@ const props = defineProps({
versions: { type: Array, default: () => [] },
modelValue: { type: [Number, String], default: undefined }
})
const emit = defineEmits(['update:modelValue', 'copy-version', 'open-details', 'activate', 'reschedule', 'edit', 'remove'])
const emit = defineEmits(['update:modelValue', 'copy-version', 'create-version', 'open-details', 'activate', 'reschedule', 'edit', 'remove'])
const title = '套餐版本'
const colNo = '版本号'
@@ -4,7 +4,7 @@
* 页面内共享一份(在父页面调用一次,作为 prop 传给各步骤组件),
* 避免产品 / 产品国家 / 附加费 被重复请求。
*/
import { listProduct, listProductCountries } from '@/api/price/product'
import { listProductOptions, listProductCountries } from '@/api/price/product'
import { listSurchargeByProduct, listSurchargeOptions } from '@/api/price/surcharge'
import {
buildCountryOptions,
@@ -40,8 +40,8 @@ export function usePriceSetRefData() {
/** 加载可选产品(已生效),保留列表展示字段 */
function loadProducts() {
return listProduct({ status: '1', pageNum: 1, pageSize: 1000 }).then(res => {
productOptions.value = (res.rows || []).map(p => ({
return listProductOptions({ status: '1' }).then(res => {
productOptions.value = (res.data || []).map(p => ({
value: p.productCode,
label: `${p.productCode} ${p.productName || ''}`.trim(),
name: p.productName,
+39 -13
View File
@@ -1,6 +1,12 @@
<template>
<div class="app-container">
<el-steps :active="active" finish-status="success" align-center class="wizard-steps">
<el-steps
v-if="!newVersionMode"
:active="active"
finish-status="success"
align-center
class="wizard-steps"
>
<el-step :title="step1Title" />
<el-step :title="step2Title" />
</el-steps>
@@ -23,6 +29,7 @@
:version-no="form.version.versionNo"
:ref-data="refData"
:live-persist="livePersist"
:header-title="detailsHeaderTitle"
/>
<div class="wizard-actions">
@@ -78,13 +85,19 @@ const progressStatus = ref(undefined)
const basicRef = ref()
const detailsRef = ref()
const copyMode = computed(() => String(route.query.copyVersion || '') === '1')
/** Entered from detail: continue-config / copy-version jump to step2 — hide 上一步 */
const directStep2 = computed(() => copyMode.value || String(route.query.step || '') === '2')
/** Edit existing version: live PUT/pull/import. New create / copy-version: defer until finish. */
const livePersist = computed(() => !!form.priceSetId && !copyMode.value)
const newVersionMode = computed(() => String(route.query.newVersion || '') === '1')
/** copy / newVersion: defer DB write until finish (same as create step2) */
const deferPersist = computed(() => copyMode.value || newVersionMode.value)
/** Entered from detail: continue-config / copy-version / new-version jump to step2 */
const directStep2 = computed(() => deferPersist.value || String(route.query.step || '') === '2')
/** Edit existing version: live PUT/pull/import. New create / copy / newVersion: defer until finish. */
const livePersist = computed(() => !!form.priceSetId && !deferPersist.value)
const step1Title = '基本信息'
const step2Title = '编辑产品价格'
const detailsHeaderTitle = computed(() =>
newVersionMode.value ? '编辑产品价格' : '第 2 步 · 编辑产品价格'
)
const form = reactive({
priceSetId: undefined,
@@ -271,7 +284,9 @@ function buildPayload(opts = {}) {
}
function goBack() {
const obj = { path: '/customer/set' }
const obj = (newVersionMode.value || copyMode.value) && form.priceSetId
? { path: '/customer/setDetail', query: { priceSetId: form.priceSetId } }
: { path: '/customer/set' }
if (proxy.$tab) {
proxy.$tab.closeOpenPage(obj)
} else {
@@ -322,7 +337,7 @@ async function onPriceBuildModeUpdate(mode) {
}
}
function applySetData(data, asCopy) {
function applySetData(data, asCopy, asNewVersion) {
form.priceSetId = data.priceSetId
Object.assign(form.basic, {
priceSetName: data.priceSetName,
@@ -341,22 +356,33 @@ function applySetData(data, asCopy) {
form.version.effectiveTime = undefined
form.version.expireTime = undefined
form.version.priceBuildMode = undefined
form.details = []
form.products = []
} else if (asNewVersion) {
// Pre-created empty draft from detail page; same step2 UX as create package
form.version.psvId = current.psvId
form.version.versionNo = current.versionNo || nextVersionNo(versions)
form.version.effectiveTime = undefined
form.version.expireTime = undefined
form.version.priceBuildMode = undefined
form.details = []
form.products = []
} else {
form.version.psvId = current.psvId
form.version.versionNo = current.versionNo || nextVersionNo(versions)
form.version.effectiveTime = current.effectiveTime
form.version.expireTime = current.expireTime
form.version.priceBuildMode = current.priceBuildMode || undefined
form.details = []
form.products = (current.products || []).map(p => ({ ...p }))
}
form.details = []
form.products = asCopy ? [] : ((current.products || []).map(p => ({ ...p })))
form.customers = (data.customers || []).map(c => ({ ...c }))
}
function loadExisting(id, asCopy) {
function loadExisting(id, asCopy, asNewVersion) {
getPriceSet(id).then(res => {
applySetData(res.data || {}, asCopy)
if (asCopy || String(route.query.step || '') === '2') {
applySetData(res.data || {}, asCopy, asNewVersion)
if (asCopy || asNewVersion || String(route.query.step || '') === '2') {
active.value = 1
}
})
@@ -366,7 +392,7 @@ onMounted(() => {
refData.loadProducts().finally(() => {
const id = route.query.priceSetId
if (id) {
loadExisting(id, copyMode.value)
loadExisting(id, copyMode.value, newVersionMode.value)
}
})
})
+176 -13
View File
@@ -24,6 +24,7 @@
v-model="currentPsvId"
:versions="set.versions || []"
@copy-version="onCopyVersion"
@create-version="onCreateVersion"
@open-details="onOpenVersionDetails"
@activate="onActivateVersion"
@reschedule="onRescheduleVersion"
@@ -52,9 +53,15 @@
<el-date-picker
v-model="activateForm.effectiveTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:00"
value-format="YYYY-MM-DD HH:00:00"
popper-class="activate-hour-picker"
:placeholder="activatePh"
:disabled-date="disabledActivateDate"
:disabled-hours="disabledActivateHours"
style="width: 100%"
@change="onActivateTimeChange"
@visible-change="onActivatePickerVisible"
/>
</el-form-item>
<div class="activate-hint">{{ activateDialogHint }}</div>
@@ -77,11 +84,13 @@ import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.v
import PriceEditPanel from './components/create/PriceEditPanel.vue'
import { usePriceSetRefData } from './composables/usePriceSetRefData'
import {
getPriceSet, disablePriceSet, enablePriceSet, activatePriceSetVersion, copyPriceSetVersion, deletePriceSetVersion
getPriceSet, disablePriceSet, enablePriceSet, activatePriceSetVersion, copyPriceSetVersion, createPriceSetVersion, deletePriceSetVersion
} from '@/api/price/set'
import { isDraftVersion } from './utils/priceSet'
const { proxy } = getCurrentInstance()
const route = useRoute()
const router = useRouter()
const refData = usePriceSetRefData()
const loading = ref(false)
@@ -124,11 +133,35 @@ function onDisable() {
function onEnable() {
confirmAction('启用后套餐将重新参与计费命中,确认启用?', () => enablePriceSet(set.value.priceSetId), '已启用')
}
/** Effective time precision: hour only (mm:ss always 00) */
function pad2(n) {
return String(n).padStart(2, '0')
}
function formatHour(d) {
if (!d || Number.isNaN(d.getTime())) return undefined
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
+ ' ' + pad2(d.getHours()) + ':00:00'
}
function normalizeToHour(val) {
if (val == null || val === '') return val
if (val instanceof Date) return formatHour(val)
const s = String(val).trim().replace('T', ' ')
const m = s.match(/^(\d{4}-\d{2}-\d{2})[ ](\d{2})/)
if (m) return m[1] + ' ' + m[2] + ':00:00'
return formatHour(new Date(s.replace(/-/g, '/'))) || val
}
function formatNow() {
const d = new Date()
const p = n => String(n).padStart(2, '0')
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate())
+ ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
return formatHour(new Date())
}
function onActivateTimeChange(val) {
activateForm.effectiveTime = normalizeToHour(val)
if (activateFormRef.value) {
activateFormRef.value.validateField('effectiveTime')
}
}
const activateVisible = ref(false)
@@ -147,13 +180,93 @@ const activateDialogTitle = computed(() =>
const activateConfirmText = computed(() =>
activateMode.value === 'reschedule' ? '确认修改' : '确认启动'
)
const activateDialogHint = computed(() =>
activateMode.value === 'reschedule'
const activatePrevEffectiveTime = ref(undefined)
const activateEffBeforePrevMsg = '生效时间不能早于上一版本的生效时间'
const activateDialogHint = computed(() => {
const base = activateMode.value === 'reschedule'
? '仅待生效版本可修改;若时间晚于当前仍为待生效,到时自动生效并使上一版本失效。'
: '若生效时间晚于当前,版本将进入「待生效」;到时自动生效并使上一版本失效。'
)
const prev = activatePrevEffectiveTime.value
if (!prev) return base
return base + '不得早于上一版:' + prev + '。'
})
const activateRules = {
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }]
effectiveTime: [
{ required: true, message: '请选择生效时间', trigger: 'change' },
{
validator: (_r, v, cb) => {
const prev = activatePrevEffectiveTime.value
if (!v || !prev) return cb()
if (toTimeMs(v) < toTimeMs(prev)) return cb(new Error(activateEffBeforePrevMsg))
cb()
},
trigger: 'change'
}
]
}
function toTimeMs(val) {
if (val == null || val === '') return NaN
const n = normalizeToHour(val)
if (!n) return NaN
return new Date(String(n).replace(/-/g, '/')).getTime()
}
function findPrevVersionEffectiveTime(row) {
if (!row) return undefined
const versions = set.value.versions || []
const curSeq = Number(row.versionSeq) || 0
let prev = null
for (const v of versions) {
if (!v || v.psvId == null || String(v.psvId) === String(row.psvId)) continue
const vs = Number(v.versionSeq) || 0
if (vs >= curSeq || !v.effectiveTime) continue
if (!prev || vs > (Number(prev.versionSeq) || 0)) prev = v
}
return prev ? normalizeToHour(prev.effectiveTime) : undefined
}
function pickActivateDefaultTime(prev) {
const now = formatNow()
if (!prev) return now
return toTimeMs(now) >= toTimeMs(prev) ? now : prev
}
function disabledActivateDate(date) {
const prev = activatePrevEffectiveTime.value
if (!prev || !date) return false
const prevDay = new Date(String(prev).replace(/-/g, '/'))
prevDay.setHours(0, 0, 0, 0)
const d = new Date(date)
d.setHours(0, 0, 0, 0)
return d.getTime() < prevDay.getTime()
}
function disabledActivateHours() {
const prev = activatePrevEffectiveTime.value
const cur = activateForm.effectiveTime
if (!prev) return []
const prevD = new Date(String(prev).replace(/-/g, '/'))
const curD = cur
? new Date(String(normalizeToHour(cur)).replace(/-/g, '/'))
: new Date()
if (
prevD.getFullYear() !== curD.getFullYear() ||
prevD.getMonth() !== curD.getMonth() ||
prevD.getDate() !== curD.getDate()
) {
return []
}
const minH = prevD.getHours()
const hours = []
for (let h = 0; h < minH; h++) hours.push(h)
return hours
}
function onActivatePickerVisible(visible) {
if (visible && activateFormRef.value) {
activateFormRef.value.validateField('effectiveTime')
}
}
function onActivateVersion(row) {
@@ -161,7 +274,8 @@ function onActivateVersion(row) {
activateMode.value = 'activate'
activateForm.psvId = row.psvId
activateForm.versionNo = row.versionNo || ''
activateForm.effectiveTime = formatNow()
activatePrevEffectiveTime.value = findPrevVersionEffectiveTime(row)
activateForm.effectiveTime = pickActivateDefaultTime(activatePrevEffectiveTime.value)
activateVisible.value = true
}
@@ -170,7 +284,14 @@ function onRescheduleVersion(row) {
activateMode.value = 'reschedule'
activateForm.psvId = row.psvId
activateForm.versionNo = row.versionNo || ''
activateForm.effectiveTime = row.effectiveTime || formatNow()
activatePrevEffectiveTime.value = findPrevVersionEffectiveTime(row)
const cur = normalizeToHour(row.effectiveTime)
const prev = activatePrevEffectiveTime.value
if (cur && (!prev || toTimeMs(cur) >= toTimeMs(prev))) {
activateForm.effectiveTime = cur
} else {
activateForm.effectiveTime = pickActivateDefaultTime(prev)
}
activateVisible.value = true
}
@@ -179,12 +300,14 @@ function onActivateClosed() {
activateForm.psvId = undefined
activateForm.versionNo = ''
activateForm.effectiveTime = undefined
activatePrevEffectiveTime.value = undefined
activateFormRef.value && activateFormRef.value.clearValidate()
}
function submitActivate() {
activateFormRef.value.validate(valid => {
if (!valid) return
activateForm.effectiveTime = normalizeToHour(activateForm.effectiveTime)
activateSubmitting.value = true
activatePriceSetVersion(activateForm.psvId, { effectiveTime: activateForm.effectiveTime })
.then(() => {
@@ -202,6 +325,23 @@ function submitActivate() {
})
})
}
function onCreateVersion() {
const priceSetId = set.value.priceSetId
if (priceSetId == null) return
const hasDraft = (set.value.versions || []).some(v => v && String(v.status) === '0')
if (hasDraft) {
proxy.$modal.msgWarning('已有草稿版本,请先启动或删除后再新建')
return
}
createPriceSetVersion(priceSetId).then(res => {
const newPsvId = res.data
proxy.$modal.msgSuccess('已创建草稿版本,请继续编辑产品价格')
router.push({
path: '/customer/setCreate',
query: { priceSetId, psvId: newPsvId, step: '2', newVersion: '1' }
})
}).catch(() => {})
}
function onCopyVersion(row) {
if (!row || row.psvId == null) return
const hasDraft = (set.value.versions || []).some(v => v && String(v.status) === '0')
@@ -231,7 +371,19 @@ function onRemoveVersion(row) {
}).catch(() => {})
}
function onEditVersion(row) {
editPsvId.value = row && row.psvId != null ? row.psvId : undefined
if (!row || row.psvId == null) return
const priceSetId = set.value.priceSetId
const noMode = !row.priceBuildMode
const noProducts = !(row.products || []).some(p => p && p.productCode)
// Empty unfinished draft (new version, not finished): resume create step2 with build-mode choice
if (isDraftVersion(row) && noMode && noProducts && priceSetId != null) {
router.push({
path: '/customer/setCreate',
query: { priceSetId, psvId: row.psvId, step: '2', newVersion: '1' }
})
return
}
editPsvId.value = row.psvId
editVisible.value = true
}
@@ -263,3 +415,14 @@ onMounted(() => {
line-height: 1.5;
}
</style>
<style>
/* Hour-only: hide minute/second columns in activate datetime popper */
.activate-hour-picker .el-time-spinner__wrapper:nth-child(2),
.activate-hour-picker .el-time-spinner__wrapper:nth-child(3) {
display: none;
}
.activate-hour-picker .el-time-spinner__wrapper:nth-child(1) {
width: 100%;
}
</style>