套餐启用增加生效时间
This commit is contained in:
+43
-17
@@ -1,21 +1,23 @@
|
||||
package com.ruoyi.web.controller.api;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.price.search.EsBizLogSupport;
|
||||
import com.ruoyi.price.search.EsLogType;
|
||||
import com.ruoyi.price.search.IBillSearchStore;
|
||||
import com.ruoyi.web.controller.price.BillingController;
|
||||
|
||||
/**
|
||||
* Persist validation failures of quote/bill APIs to Elasticsearch.
|
||||
* Persist quote/bill API failures to Elasticsearch (validation, JSON parse, business, unexpected).
|
||||
* Uses {@link EsBizLogSupport#saveApiFail} so service/controller can also call without duplicate docs.
|
||||
*/
|
||||
@Order(0)
|
||||
@RestControllerAdvice(assignableTypes = { TmsPriceController.class, BillingController.class })
|
||||
@@ -25,37 +27,61 @@ public class PriceSyncEsLogAdvice
|
||||
private IBillSearchStore billSearchStore;
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public AjaxResult handleValid(MethodArgumentNotValidException e)
|
||||
public AjaxResult handleValid(MethodArgumentNotValidException e, HttpServletRequest request)
|
||||
{
|
||||
String message = e.getBindingResult().getFieldError() == null
|
||||
? e.getMessage()
|
||||
: e.getBindingResult().getFieldError().getDefaultMessage();
|
||||
saveFail(e.getBindingResult().getTarget(), message);
|
||||
saveFail(request, e.getBindingResult().getTarget(), message);
|
||||
return AjaxResult.error(message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(BindException.class)
|
||||
public AjaxResult handleBind(BindException e)
|
||||
public AjaxResult handleBind(BindException e, HttpServletRequest request)
|
||||
{
|
||||
String message = e.getAllErrors().isEmpty() ? e.getMessage() : e.getAllErrors().get(0).getDefaultMessage();
|
||||
saveFail(e.getTarget(), message);
|
||||
saveFail(request, e.getTarget(), message);
|
||||
return AjaxResult.error(message);
|
||||
}
|
||||
|
||||
private void saveFail(Object target, String message)
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public AjaxResult handleNotReadable(HttpMessageNotReadableException e, HttpServletRequest request)
|
||||
{
|
||||
if (target instanceof TmsPriceBillRequest)
|
||||
String message = EsBizLogSupport.errorText(e);
|
||||
saveFail(request, null, message);
|
||||
return AjaxResult.error(message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(ServiceException.class)
|
||||
public AjaxResult handleService(ServiceException e, HttpServletRequest request)
|
||||
{
|
||||
saveFail(request, null, e);
|
||||
Integer code = e.getCode();
|
||||
return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public AjaxResult handleAny(Exception e, HttpServletRequest request)
|
||||
{
|
||||
saveFail(request, null, e);
|
||||
return AjaxResult.error(EsBizLogSupport.errorText(e));
|
||||
}
|
||||
|
||||
private void saveFail(HttpServletRequest request, Object boundTarget, Throwable e)
|
||||
{
|
||||
if (!EsBizLogSupport.isPriceSyncWriteApi(request))
|
||||
{
|
||||
TmsPriceBillRequest request = (TmsPriceBillRequest) target;
|
||||
EsBizLogSupport.saveFail(billSearchStore, EsLogType.BILL, EsBizLogSupport.billFailBizId(request), request,
|
||||
message);
|
||||
return;
|
||||
}
|
||||
if (target instanceof TmsPriceQuoteRequest)
|
||||
EsBizLogSupport.saveApiFail(billSearchStore, request, boundTarget, e);
|
||||
}
|
||||
|
||||
private void saveFail(HttpServletRequest request, Object boundTarget, String message)
|
||||
{
|
||||
if (!EsBizLogSupport.isPriceSyncWriteApi(request))
|
||||
{
|
||||
TmsPriceQuoteRequest request = (TmsPriceQuoteRequest) target;
|
||||
EsBizLogSupport.saveFail(billSearchStore, EsLogType.QUOTE, EsBizLogSupport.quoteFailBizId(request),
|
||||
request, message);
|
||||
return;
|
||||
}
|
||||
EsBizLogSupport.saveApiFail(billSearchStore, request, boundTarget, message);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -1,5 +1,7 @@
|
||||
package com.ruoyi.web.controller.price;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -24,6 +26,7 @@ import com.ruoyi.price.domain.PrcCustomerPriceSet;
|
||||
import com.ruoyi.price.domain.PrcPriceSet;
|
||||
import com.ruoyi.price.domain.PrcPriceSetDetail;
|
||||
import com.ruoyi.price.domain.PrcPriceSetSaveTask;
|
||||
import com.ruoyi.price.domain.PrcPriceSetVersion;
|
||||
import com.ruoyi.price.service.IPrcPriceSetService;
|
||||
|
||||
/**
|
||||
@@ -124,9 +127,10 @@ public class PriceSetController extends BaseController
|
||||
@PreAuthorize("@ss.hasPermi('price:set:edit')")
|
||||
@Log(title = "Price set version activate", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/version/activate/{psvId}")
|
||||
public AjaxResult activateVersion(@PathVariable Long psvId)
|
||||
public AjaxResult activateVersion(@PathVariable Long psvId, @RequestBody(required = false) PrcPriceSetVersion body)
|
||||
{
|
||||
return toAjax(priceSetService.activateVersion(psvId, getUsername()));
|
||||
Date effectiveTime = body == null ? null : body.getEffectiveTime();
|
||||
return toAjax(priceSetService.activateVersion(psvId, effectiveTime, getUsername()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:set:add')")
|
||||
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
package com.ruoyi.web.controller.price;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.MessageUtils;
|
||||
import com.ruoyi.price.task.BmsStatusRefreshTask;
|
||||
|
||||
/**
|
||||
* 状态自动刷新 - 手动触发入口
|
||||
*
|
||||
* <p>日常由 BmsStatusRefreshTask 每天 02:00 跑,特殊情况可手动触发。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/price/status")
|
||||
public class PriceStatusController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private BmsStatusRefreshTask refreshTask;
|
||||
|
||||
/**
|
||||
* 立即刷新 bms_product / bms_product_country 的 status 字段
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:product:edit')")
|
||||
@PostMapping("/refresh")
|
||||
public AjaxResult refresh()
|
||||
{
|
||||
int[] counts = refreshTask.refreshNow();
|
||||
String msg = MessageUtils.message("price.status.refresh.success",
|
||||
String.valueOf(counts[0]), String.valueOf(counts[1]),
|
||||
String.valueOf(counts.length > 2 ? counts[2] : 0));
|
||||
return success(msg);
|
||||
}
|
||||
}
|
||||
@@ -126,6 +126,8 @@ price.quote.freightMissing2=产品默认运费或挂号费为空
|
||||
price.quote.freightMissing3=运费为空
|
||||
price.quote.requestBodyRequired=请求体为空
|
||||
price.quote.customerIdRequired=客户ID为空
|
||||
price.quote.customerCodeRequired=客户代码不能为空
|
||||
price.quote.customerCodeNotFound=客户代码不存在
|
||||
price.quote.productCodeRequired=产品代码为空
|
||||
price.quote.countryRequired=国家代码或国家名称为空
|
||||
price.quote.weightInvalid=重量必须大于0
|
||||
@@ -135,8 +137,6 @@ price.quote.noCustomerPackageOverride=没有客户套餐覆盖,使用产品默
|
||||
price.quote.quoteIdRequired=报价ID为空
|
||||
price.quote.hitCustomerFixedPackageDetail=命中客户套餐固定价格明细
|
||||
|
||||
# ---- 状态自动刷新 ----
|
||||
price.status.refresh.success=状态刷新完成:bms_product 更新 {0} 行,bms_product_country 更新 {1} 行,价格套餐版本 {2} 套
|
||||
# receivable bill
|
||||
price.bill.orderNoRequired=订单号为空
|
||||
price.bill.billIdRequired=计费ID为空
|
||||
@@ -147,6 +147,7 @@ price.set.version.effectiveTime.empty=版本生效时间不能为空
|
||||
price.set.version.notExists=套餐版本不存在
|
||||
price.set.version.activate.statusNotAllowed=仅草稿版本可启动
|
||||
price.set.version.delete.statusNotAllowed=仅草稿版本可删除
|
||||
price.set.version.copy.draftExists=已有草稿版本,请先启动或删除后再复制
|
||||
price.set.save.task.notExists=保存任务不存在或已过期
|
||||
price.set.library.empty=产品库无可用价格明细,请检查产品国家配置
|
||||
price.set.detail.duplicate.fixed=产品{0} 国家{1} 重量段{2} {3}(共{4}条)
|
||||
|
||||
@@ -24,7 +24,7 @@ public class PrcPriceSetVersion extends BaseEntity
|
||||
/** null = open; set to next version effective_time when that version starts */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date expireTime;
|
||||
/** 0 draft / 1 active / 2 expired */
|
||||
/** 0 draft / 3 pending / 1 active / 2 expired */
|
||||
private String status;
|
||||
|
||||
private List<PrcPriceSetDetail> details;
|
||||
|
||||
@@ -18,6 +18,8 @@ public class PrcQuote extends BaseEntity
|
||||
private String quoteId;
|
||||
private String quoteKey;
|
||||
private Long customerId;
|
||||
/** bms_customer.customer_code */
|
||||
private String customerCode;
|
||||
private String productCode;
|
||||
private String productName;
|
||||
private String countryCode;
|
||||
|
||||
+2
@@ -14,6 +14,8 @@ public class TmsPriceQuoteRequest implements Serializable
|
||||
|
||||
private Long customerId;
|
||||
|
||||
/** Required. Maps to bms_customer.customer_code; used in quote unique key. */
|
||||
@NotBlank(message = "customerCode is required")
|
||||
private String customerCode;
|
||||
|
||||
@NotBlank(message = "productCode is required")
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.ruoyi.price.search;
|
||||
|
||||
import java.util.UUID;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.http.HttpHelper;
|
||||
import com.ruoyi.common.utils.sign.Md5Utils;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest;
|
||||
@@ -14,7 +18,11 @@ public final class EsBizLogSupport
|
||||
public static final String STATUS_SUCCESS = "SUCCESS";
|
||||
public static final String STATUS_FAIL = "FAIL";
|
||||
|
||||
/** Request attribute: fail already written to ES in this request. */
|
||||
public static final String ATTR_FAIL_LOGGED = "bms.es.fail.logged";
|
||||
|
||||
private static final int ERROR_MAX = 2000;
|
||||
private static final int RAW_BODY_MAX = 8000;
|
||||
|
||||
private EsBizLogSupport()
|
||||
{
|
||||
@@ -26,6 +34,10 @@ public final class EsBizLogSupport
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (request instanceof CharSequence)
|
||||
{
|
||||
return cut(String.valueOf(request), RAW_BODY_MAX);
|
||||
}
|
||||
JSONObject obj = JSON.parseObject(JSON.toJSONString(request));
|
||||
if (obj == null)
|
||||
{
|
||||
@@ -64,15 +76,20 @@ public final class EsBizLogSupport
|
||||
}
|
||||
|
||||
public static String cut(String msg)
|
||||
{
|
||||
return cut(msg, ERROR_MAX);
|
||||
}
|
||||
|
||||
public static String cut(String msg, int max)
|
||||
{
|
||||
if (msg == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String text = msg.trim();
|
||||
if (text.length() > ERROR_MAX)
|
||||
if (text.length() > max)
|
||||
{
|
||||
return text.substring(0, ERROR_MAX);
|
||||
return text.substring(0, max);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -104,6 +121,39 @@ public final class EsBizLogSupport
|
||||
return "FAIL-" + Md5Utils.hash(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common entry for quote/bill API failures (validation / JSON parse / business / unexpected).
|
||||
* Dedups with {@link #ATTR_FAIL_LOGGED} so controller/service/advice can all call safely.
|
||||
*/
|
||||
public static void saveApiFail(IBillSearchStore store, HttpServletRequest httpRequest, Object boundTarget,
|
||||
Throwable e)
|
||||
{
|
||||
saveApiFail(store, httpRequest, boundTarget, errorText(e));
|
||||
}
|
||||
|
||||
public static void saveApiFail(IBillSearchStore store, HttpServletRequest httpRequest, Object boundTarget,
|
||||
String errorMessage)
|
||||
{
|
||||
if (store == null || isFailLogged(httpRequest))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String rawBody = readRawBody(httpRequest);
|
||||
String logType = resolveLogType(httpRequest, boundTarget);
|
||||
if (StringUtils.isEmpty(logType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Object payload = boundTarget != null ? boundTarget : rawBody;
|
||||
String bizId = resolveFailBizId(logType, boundTarget, rawBody);
|
||||
// saveFail also marks ATTR_FAIL_LOGGED
|
||||
saveFail(store, logType, bizId, payload, errorMessage);
|
||||
if (!isFailLogged(httpRequest))
|
||||
{
|
||||
markFailLogged(httpRequest);
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveFail(IBillSearchStore store, String logType, String bizId, Object request, Throwable e)
|
||||
{
|
||||
saveFail(store, logType, bizId, request, errorText(e));
|
||||
@@ -115,8 +165,157 @@ public final class EsBizLogSupport
|
||||
{
|
||||
return;
|
||||
}
|
||||
HttpServletRequest httpRequest = currentRequest();
|
||||
if (isFailLogged(httpRequest))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String msg = cut(errorMessage);
|
||||
store.save(logType, bizId, requestJson(request), failResponseJson(msg), STATUS_FAIL, msg);
|
||||
markFailLogged(httpRequest);
|
||||
}
|
||||
|
||||
public static String resolveLogType(HttpServletRequest httpRequest, Object boundTarget)
|
||||
{
|
||||
if (boundTarget instanceof TmsPriceBillRequest)
|
||||
{
|
||||
return EsLogType.BILL;
|
||||
}
|
||||
if (boundTarget instanceof TmsPriceQuoteRequest)
|
||||
{
|
||||
return EsLogType.QUOTE;
|
||||
}
|
||||
if (httpRequest == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String uri = httpRequest.getRequestURI();
|
||||
if (uri == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (uri.endsWith("/bill"))
|
||||
{
|
||||
return EsLogType.BILL;
|
||||
}
|
||||
if (uri.endsWith("/quote"))
|
||||
{
|
||||
return EsLogType.QUOTE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String resolveFailBizId(String logType, Object boundTarget, String rawBody)
|
||||
{
|
||||
if (EsLogType.BILL.equals(logType))
|
||||
{
|
||||
if (boundTarget instanceof TmsPriceBillRequest)
|
||||
{
|
||||
return billFailBizId((TmsPriceBillRequest) boundTarget);
|
||||
}
|
||||
String orderNo = extractJsonField(rawBody, "orderNo");
|
||||
if (StringUtils.isNotEmpty(orderNo))
|
||||
{
|
||||
return orderNo.trim();
|
||||
}
|
||||
return "FAIL-" + UUID.randomUUID();
|
||||
}
|
||||
if (EsLogType.QUOTE.equals(logType))
|
||||
{
|
||||
if (boundTarget instanceof TmsPriceQuoteRequest)
|
||||
{
|
||||
return quoteFailBizId((TmsPriceQuoteRequest) boundTarget);
|
||||
}
|
||||
String seed = StringUtils.isNotEmpty(rawBody) ? rawBody : UUID.randomUUID().toString();
|
||||
return "FAIL-" + Md5Utils.hash(seed);
|
||||
}
|
||||
return "FAIL-" + UUID.randomUUID();
|
||||
}
|
||||
|
||||
public static boolean isPriceSyncWriteApi(HttpServletRequest httpRequest)
|
||||
{
|
||||
if (httpRequest == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!"POST".equalsIgnoreCase(httpRequest.getMethod()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
String uri = httpRequest.getRequestURI();
|
||||
if (uri == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return uri.endsWith("/quote") || uri.endsWith("/bill");
|
||||
}
|
||||
|
||||
public static String readRawBody(HttpServletRequest httpRequest)
|
||||
{
|
||||
if (httpRequest == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
String body = HttpHelper.getBodyString(httpRequest);
|
||||
return cut(body, RAW_BODY_MAX);
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isFailLogged(HttpServletRequest httpRequest)
|
||||
{
|
||||
HttpServletRequest req = httpRequest != null ? httpRequest : currentRequest();
|
||||
return req != null && Boolean.TRUE.equals(req.getAttribute(ATTR_FAIL_LOGGED));
|
||||
}
|
||||
|
||||
public static void markFailLogged(HttpServletRequest httpRequest)
|
||||
{
|
||||
HttpServletRequest req = httpRequest != null ? httpRequest : currentRequest();
|
||||
if (req != null)
|
||||
{
|
||||
req.setAttribute(ATTR_FAIL_LOGGED, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpServletRequest currentRequest()
|
||||
{
|
||||
try
|
||||
{
|
||||
ServletRequestAttributes attrs =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
return attrs == null ? null : attrs.getRequest();
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractJsonField(String rawBody, String field)
|
||||
{
|
||||
if (StringUtils.isEmpty(rawBody) || StringUtils.isEmpty(field))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
JSONObject obj = JSON.parseObject(rawBody);
|
||||
if (obj == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Object value = obj.get(field);
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String nvl(Object value)
|
||||
|
||||
+7
-3
@@ -1,5 +1,6 @@
|
||||
package com.ruoyi.price.service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.ruoyi.price.domain.PrcCustomerPriceSet;
|
||||
import com.ruoyi.price.domain.PrcPriceSet;
|
||||
@@ -32,8 +33,11 @@ public interface IPrcPriceSetService
|
||||
|
||||
public int disable(Long priceSetId, String updateBy);
|
||||
|
||||
/** Activate a draft version: set effective_time=now, refresh version 0/1/2 */
|
||||
public int activateVersion(Long psvId, String updateBy);
|
||||
/**
|
||||
* Activate a draft version with user-specified effectiveTime.
|
||||
* effectiveTime <= now -> EFFECT; future -> PENDING(3). Then refresh 0/3/1/2.
|
||||
*/
|
||||
public int activateVersion(Long psvId, Date effectiveTime, String updateBy);
|
||||
|
||||
/**
|
||||
* Copy source version into a new draft version under the same package:
|
||||
@@ -65,7 +69,7 @@ public interface IPrcPriceSetService
|
||||
|
||||
public int deleteCustomerByIds(Long[] customerPricesetIds);
|
||||
|
||||
/** chain expire_time = next version effective_time, then refresh 0/1/2 */
|
||||
/** chain expire_time = next version effective_time, then refresh 0/3/1/2 */
|
||||
public int refreshAllVersionStatus();
|
||||
|
||||
/** submit async save, return taskId immediately */
|
||||
|
||||
+14
-3
@@ -128,8 +128,18 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
bill.setBillKey(billKey);
|
||||
bill.setOrderNo(orderNo);
|
||||
bill.setCustomerId(request.getCustomerId() == null ? Long.valueOf(0L) : request.getCustomerId());
|
||||
bill.setCustomerCode(request.getCustomerCode());
|
||||
Long customerId = request.getCustomerId();
|
||||
if (customerId == null && result != null)
|
||||
{
|
||||
customerId = result.getCustomerId();
|
||||
}
|
||||
bill.setCustomerId(customerId == null ? Long.valueOf(0L) : customerId);
|
||||
String customerCode = request.getCustomerCode();
|
||||
if (StringUtils.isEmpty(customerCode) && result != null)
|
||||
{
|
||||
customerCode = result.getCustomerCode();
|
||||
}
|
||||
bill.setCustomerCode(customerCode);
|
||||
bill.setCustomerName(request.getCustomerName());
|
||||
bill.setTrackingNo(request.getTrackingNo());
|
||||
bill.setPieces(request.getPieces());
|
||||
@@ -178,7 +188,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
bill.setRoundMode(result.getRoundMode());
|
||||
result.setBillId(bill.getBillId());
|
||||
result.setOrderNo(orderNo);
|
||||
result.setCustomerCode(request.getCustomerCode());
|
||||
result.setCustomerId(bill.getCustomerId());
|
||||
result.setCustomerCode(bill.getCustomerCode());
|
||||
result.setIncomeTotal(bill.getIncomeTotal());
|
||||
result.setPayTotal(bill.getPayTotal());
|
||||
result.setPayableTotal(bill.getIncomeTotal());
|
||||
|
||||
+80
-51
@@ -57,6 +57,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
private static final String TYPE_DISCOUNT = "DISCOUNT";
|
||||
private static final String VER_DRAFT = "0";
|
||||
private static final String VER_EFFECT = "1";
|
||||
/** Pending: effective_time set but not yet reached */
|
||||
private static final String VER_PENDING = "3";
|
||||
private static final String VER_EXPIRED = "2";
|
||||
|
||||
@Autowired
|
||||
@@ -93,6 +95,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
PrcPriceSet set = setMapper.selectPrcPriceSetById(priceSetId);
|
||||
if (set != null)
|
||||
{
|
||||
// Reconcile version status/expire windows so UI always shows at most one EFFECT
|
||||
refreshVersionStatus(priceSetId);
|
||||
fillExpired(set);
|
||||
fillVersions(set);
|
||||
}
|
||||
@@ -268,12 +272,16 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int activateVersion(Long psvId, String updateBy)
|
||||
public int activateVersion(Long psvId, Date effectiveTime, String updateBy)
|
||||
{
|
||||
if (psvId == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.version.notExists"));
|
||||
}
|
||||
if (effectiveTime == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.version.effectiveTime.empty"));
|
||||
}
|
||||
PrcPriceSetVersion version = versionMapper.selectPrcPriceSetVersionById(psvId);
|
||||
if (version == null)
|
||||
{
|
||||
@@ -288,34 +296,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.edit.stopped"));
|
||||
}
|
||||
Date now = new Date();
|
||||
// Expire currently effective versions under the same package
|
||||
PrcPriceSetVersion q = new PrcPriceSetVersion();
|
||||
q.setPriceSetId(set.getPriceSetId());
|
||||
List<PrcPriceSetVersion> siblings = versionMapper.selectPrcPriceSetVersionList(q);
|
||||
if (siblings != null)
|
||||
{
|
||||
for (PrcPriceSetVersion other : siblings)
|
||||
{
|
||||
if (other.getPsvId() == null || other.getPsvId().equals(psvId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (VER_EFFECT.equals(other.getStatus()))
|
||||
{
|
||||
other.setStatus(VER_EXPIRED);
|
||||
other.setExpireTime(now);
|
||||
other.setUpdateBy(updateBy);
|
||||
versionMapper.updatePrcPriceSetVersion(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
version.setEffectiveTime(now);
|
||||
// Set schedule; refreshVersionStatus decides PENDING(3) vs EFFECT(1) and chains expire_time
|
||||
version.setEffectiveTime(effectiveTime);
|
||||
version.setExpireTime(null);
|
||||
version.setStatus(VER_EFFECT);
|
||||
version.setUpdateBy(updateBy);
|
||||
versionMapper.updatePrcPriceSetVersion(version);
|
||||
|
||||
refreshVersionStatus(set.getPriceSetId());
|
||||
return 1;
|
||||
}
|
||||
@@ -338,6 +323,10 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.edit.stopped"));
|
||||
}
|
||||
if (hasDraftVersion(set.getPriceSetId()))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.version.copy.draftExists"));
|
||||
}
|
||||
|
||||
Integer maxSeq = versionMapper.selectMaxVersionSeq(set.getPriceSetId());
|
||||
int seq = maxSeq == null ? 1 : maxSeq.intValue() + 1;
|
||||
@@ -1925,6 +1914,34 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
version.setSurcharges(saved);
|
||||
}
|
||||
|
||||
/** True when package already has a true draft (status=0). Pending(3) does not block copy. */
|
||||
private boolean hasDraftVersion(Long priceSetId)
|
||||
{
|
||||
if (priceSetId == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
PrcPriceSetVersion q = new PrcPriceSetVersion();
|
||||
q.setPriceSetId(priceSetId);
|
||||
List<PrcPriceSetVersion> list = versionMapper.selectPrcPriceSetVersionList(q);
|
||||
if (list == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (PrcPriceSetVersion v : list)
|
||||
{
|
||||
if (v == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (VER_DRAFT.equals(v.getStatus()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void refreshVersionStatus(Long priceSetId)
|
||||
{
|
||||
PrcPriceSetVersion q = new PrcPriceSetVersion();
|
||||
@@ -1958,10 +1975,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return Integer.compare(nullToZero(a.getVersionSeq()), nullToZero(b.getVersionSeq()));
|
||||
});
|
||||
Date now = new Date();
|
||||
// First pass: chain expire_time = next sibling's effective_time; compute window status
|
||||
Long effectPsvId = null;
|
||||
for (int i = 0; i < versions.size(); i++)
|
||||
{
|
||||
PrcPriceSetVersion v = versions.get(i);
|
||||
// Next start only among versions that already have effective_time
|
||||
Date nextStart = null;
|
||||
for (int j = i + 1; j < versions.size(); j++)
|
||||
{
|
||||
@@ -1972,46 +1990,57 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
break;
|
||||
}
|
||||
}
|
||||
String status;
|
||||
v.setExpireTime(nextStart);
|
||||
if (v.getEffectiveTime() == null)
|
||||
{
|
||||
status = VER_DRAFT;
|
||||
v.setStatus(VER_DRAFT);
|
||||
continue;
|
||||
}
|
||||
else if (startedNotAfter(v.getEffectiveTime(), now)
|
||||
&& (nextStart == null || nextStart.after(now)))
|
||||
// Not started yet -> pending
|
||||
if (v.getEffectiveTime().getTime() > now.getTime())
|
||||
{
|
||||
status = VER_EFFECT;
|
||||
v.setStatus(VER_PENDING);
|
||||
continue;
|
||||
}
|
||||
else if (startedNotAfter(v.getEffectiveTime(), now))
|
||||
// Window ended: expire_time <= now (same rule as bill: expire_time > asOf to stay active)
|
||||
if (nextStart != null && nextStart.getTime() <= now.getTime())
|
||||
{
|
||||
status = VER_EXPIRED;
|
||||
v.setStatus(VER_EXPIRED);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
// In window: candidate for the single EFFECT version (latest wins)
|
||||
v.setStatus(VER_EFFECT);
|
||||
effectPsvId = v.getPsvId();
|
||||
}
|
||||
// Second pass: only the latest in-window version stays EFFECT
|
||||
if (effectPsvId != null)
|
||||
{
|
||||
for (PrcPriceSetVersion v : versions)
|
||||
{
|
||||
// future-dated: keep as draft until wall-clock reaches effective_time
|
||||
status = VER_DRAFT;
|
||||
if (VER_EFFECT.equals(v.getStatus())
|
||||
&& v.getPsvId() != null
|
||||
&& !v.getPsvId().equals(effectPsvId))
|
||||
{
|
||||
v.setStatus(VER_EXPIRED);
|
||||
}
|
||||
}
|
||||
boolean statusChanged = !status.equals(v.getStatus());
|
||||
boolean expireChanged = !sameTime(nextStart, v.getExpireTime());
|
||||
}
|
||||
for (PrcPriceSetVersion v : versions)
|
||||
{
|
||||
PrcPriceSetVersion db = versionMapper.selectPrcPriceSetVersionById(v.getPsvId());
|
||||
if (db == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
boolean statusChanged = !String.valueOf(v.getStatus()).equals(String.valueOf(db.getStatus()));
|
||||
boolean expireChanged = !sameTime(v.getExpireTime(), db.getExpireTime());
|
||||
if (statusChanged || expireChanged)
|
||||
{
|
||||
v.setStatus(status);
|
||||
v.setExpireTime(nextStart);
|
||||
versionMapper.updatePrcPriceSetVersion(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** effective_time <= now (+1s skew tolerance) */
|
||||
private static boolean startedNotAfter(Date effectiveTime, Date now)
|
||||
{
|
||||
if (effectiveTime == null || now == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return effectiveTime.getTime() <= now.getTime() + 1000L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int refreshAllVersionStatus()
|
||||
{
|
||||
|
||||
+65
-4
@@ -21,6 +21,8 @@ import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.MessageUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.sign.Md5Utils;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomer;
|
||||
import com.ruoyi.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||
import com.ruoyi.price.domain.BmsProduct;
|
||||
import com.ruoyi.price.domain.BmsProductCountry;
|
||||
import com.ruoyi.price.domain.PrcCustomerPriceSet;
|
||||
@@ -33,6 +35,7 @@ import com.ruoyi.price.domain.PrcQuoteFee;
|
||||
import com.ruoyi.price.domain.PrcSurchargeRel;
|
||||
import com.ruoyi.price.domain.PrcZone;
|
||||
import com.ruoyi.price.mapper.PrcZoneDetailMapper;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteFeeLine;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
|
||||
@@ -106,6 +109,9 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
@Autowired
|
||||
private IBillSearchStore billSearchStore;
|
||||
|
||||
@Autowired
|
||||
private BmsCustomerMapper bmsCustomerMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public TmsPriceQuoteResult quote(TmsPriceQuoteRequest request)
|
||||
@@ -125,6 +131,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
private TmsPriceQuoteResult quoteInternal(TmsPriceQuoteRequest request)
|
||||
{
|
||||
validateRequest(request);
|
||||
resolveCustomer(request);
|
||||
Date bizDate = resolveBizDate(request.getBizDate());
|
||||
BigDecimal weight = request.getWeight().setScale(3, RoundingMode.HALF_UP);
|
||||
String quoteKey = buildQuoteKey(request, weight, bizDate);
|
||||
@@ -156,6 +163,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
public TmsPriceQuoteResult calculate(TmsPriceQuoteRequest request)
|
||||
{
|
||||
validateRequest(request);
|
||||
resolveCustomer(request);
|
||||
Date bizDate = resolveBizDate(request.getBizDate());
|
||||
TmsPriceQuoteResult result = doQuote(request, bizDate);
|
||||
applySurchargesAndFees(result);
|
||||
@@ -312,6 +320,53 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
result.setDestPostcode(emptyToNull(request.getDestPostcode()));
|
||||
}
|
||||
// Always prefer customer resolved from customerCode (ignore TMS/bind ids)
|
||||
if (request.getCustomerId() != null)
|
||||
{
|
||||
result.setCustomerId(request.getCustomerId());
|
||||
}
|
||||
if (StringUtils.isNotEmpty(request.getCustomerCode()))
|
||||
{
|
||||
result.setCustomerCode(request.getCustomerCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require customerCode; resolve customer_id from bms_customer.
|
||||
*/
|
||||
private void resolveCustomer(TmsPriceQuoteRequest request)
|
||||
{
|
||||
if (request == null || StringUtils.isEmpty(request.getCustomerCode()))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.quote.customerCodeRequired"));
|
||||
}
|
||||
String code = request.getCustomerCode().trim();
|
||||
if (StringUtils.isEmpty(code))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.quote.customerCodeRequired"));
|
||||
}
|
||||
request.setCustomerCode(code);
|
||||
BmsCustomer query = new BmsCustomer();
|
||||
query.setCustomerCode(code);
|
||||
List<BmsCustomer> list = bmsCustomerMapper.selectBmsCustomerList(query);
|
||||
if (list == null || list.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.quote.customerCodeNotFound"));
|
||||
}
|
||||
BmsCustomer hit = list.get(0);
|
||||
request.setCustomerId(hit.getCustomerId());
|
||||
if (StringUtils.isNotEmpty(hit.getCustomerCode()))
|
||||
{
|
||||
request.setCustomerCode(hit.getCustomerCode());
|
||||
}
|
||||
if (request instanceof TmsPriceBillRequest)
|
||||
{
|
||||
TmsPriceBillRequest billReq = (TmsPriceBillRequest) request;
|
||||
if (StringUtils.isEmpty(billReq.getCustomerName()) && StringUtils.isNotEmpty(hit.getCustomerName()))
|
||||
{
|
||||
billReq.setCustomerName(hit.getCustomerName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveCountryIso2(String productCode, String countryCode, String countryName)
|
||||
@@ -684,6 +739,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
quote.setQuoteId(UUID.randomUUID().toString());
|
||||
quote.setQuoteKey(quoteKey);
|
||||
quote.setCustomerId(request.getCustomerId() == null ? Long.valueOf(0L) : request.getCustomerId());
|
||||
quote.setCustomerCode(emptyToNull(request.getCustomerCode()));
|
||||
quote.setProductCode(result.getProductCode());
|
||||
quote.setProductName(result.getProductName());
|
||||
quote.setCountryCode(result.getCountryCode());
|
||||
@@ -810,6 +866,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
r.setMatchSource(quote.getMatchSource());
|
||||
r.setMatchMessage(quote.getMatchMessage());
|
||||
r.setCustomerId(quote.getCustomerId());
|
||||
r.setCustomerCode(quote.getCustomerCode());
|
||||
r.setPriceSetId(quote.getPriceSetId());
|
||||
r.setPriceSetCode(quote.getPriceSetCode());
|
||||
r.setPriceSetName(quote.getPriceSetName());
|
||||
@@ -857,6 +914,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
to.setMatchSource(from.getMatchSource());
|
||||
to.setMatchMessage(from.getMatchMessage());
|
||||
to.setCustomerId(from.getCustomerId());
|
||||
to.setCustomerCode(from.getCustomerCode());
|
||||
to.setPriceSetId(from.getPriceSetId());
|
||||
to.setPriceSetCode(from.getPriceSetCode());
|
||||
to.setPriceSetName(from.getPriceSetName());
|
||||
@@ -892,6 +950,10 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
|
||||
private String buildQuoteKey(TmsPriceQuoteRequest request, BigDecimal weight, Date bizDate)
|
||||
{
|
||||
if (StringUtils.isEmpty(request.getCustomerCode()))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.quote.customerCodeRequired"));
|
||||
}
|
||||
String countryCode = emptyToNull(request.getCountryCode());
|
||||
if (countryCode != null)
|
||||
{
|
||||
@@ -899,8 +961,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
String countryName = emptyToNull(request.getCountryName());
|
||||
String biz = new SimpleDateFormat("yyyy-MM-dd").format(bizDate);
|
||||
String raw = (StringUtils.isNotEmpty(request.getCustomerCode())
|
||||
? request.getCustomerCode() : String.valueOf(request.getCustomerId())) + "|"
|
||||
String raw = request.getCustomerCode().trim() + "|"
|
||||
+ request.getProductCode().trim() + "|"
|
||||
+ nvlStr(countryCode) + "|"
|
||||
+ nvlStr(countryName) + "|"
|
||||
@@ -995,9 +1056,9 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.quote.requestBodyRequired"));
|
||||
}
|
||||
if (StringUtils.isEmpty(request.getCustomerCode()) && request.getCustomerId() == null)
|
||||
if (StringUtils.isEmpty(request.getCustomerCode()))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.quote.customerIdRequired"));
|
||||
throw new ServiceException(MessageUtils.message("price.quote.customerCodeRequired"));
|
||||
}
|
||||
if (StringUtils.isEmpty(request.getProductCode()))
|
||||
{
|
||||
|
||||
+16
-47
@@ -7,73 +7,42 @@ import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.ruoyi.price.mapper.BmsProductCountryMapper;
|
||||
import com.ruoyi.price.mapper.BmsProductMapper;
|
||||
import com.ruoyi.price.service.IPrcPriceSetService;
|
||||
|
||||
/**
|
||||
* bms_product / bms_product_country 状态自动刷新
|
||||
*
|
||||
* <p>每天 02:00 根据 effective_date / expire_date 与当天日期,回填 status:
|
||||
* <ul>
|
||||
* <li>'2' 已失效 EXPIRED:expire_date 非空且 <= 今天</li>
|
||||
* <li>'0' 待生效 PENDING:effective_date 为空 / 未来</li>
|
||||
* <li>'1' 已生效 ACTIVE:其余(已生效且仍在有效期)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>对应字典 bms_product_effect_status 的 value=ACTIVE/PENDING/EXPIRED。
|
||||
* 实际存储使用 0/1/2 与列 comment 保持一致;前端 dict-tag 需做 value 映射或调整 dict_value。
|
||||
*
|
||||
* <p>手动触发:调用 refreshNow()(运维/测试用,详见 StatusRefreshController)。
|
||||
*
|
||||
* @author ruoyi
|
||||
* Refresh prc_price_set_version status (draft/pending/effect/expired).
|
||||
* Startup + every 5 minutes so PENDING becomes EFFECT when effective_time is reached.
|
||||
*/
|
||||
@Component
|
||||
public class BmsStatusRefreshTask implements ApplicationRunner
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(BmsStatusRefreshTask.class);
|
||||
|
||||
@Autowired
|
||||
private BmsProductMapper productMapper;
|
||||
|
||||
@Autowired
|
||||
private BmsProductCountryMapper productCountryMapper;
|
||||
|
||||
@Autowired
|
||||
private IPrcPriceSetService priceSetService;
|
||||
|
||||
/** 每天 02:00 跑(cron: sec min hour DOM month DOW) */
|
||||
@Scheduled(cron = "0 0 2 * * ?")
|
||||
public void scheduled()
|
||||
{
|
||||
refreshNow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args)
|
||||
{
|
||||
refresh("startup");
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0/5 * * * ?")
|
||||
public void scheduledRefresh()
|
||||
{
|
||||
refresh("scheduled");
|
||||
}
|
||||
|
||||
private void refresh(String scene)
|
||||
{
|
||||
try
|
||||
{
|
||||
int n = priceSetService.refreshAllVersionStatus();
|
||||
log.info("startup prc_price_set_version expire/status refreshed, sets={}", n);
|
||||
log.info("{} prc_price_set_version expire/status refreshed, sets={}", scene, n);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("startup prc_price_set_version refresh failed", e);
|
||||
log.error("{} prc_price_set_version refresh failed", scene, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh product/country status and chain version expire_time.
|
||||
* Returns [product rows, country rows, price-set count].
|
||||
*/
|
||||
public int[] refreshNow()
|
||||
{
|
||||
int productCount = productMapper.refreshStatusByDates();
|
||||
int countryCount = productCountryMapper.refreshStatusByDates();
|
||||
int versionSets = priceSetService.refreshAllVersionStatus();
|
||||
log.info("BmsStatusRefreshTask done: bms_product={}, bms_product_country={}, price_set_version_sets={}",
|
||||
productCount, countryCount, versionSets);
|
||||
return new int[] { productCount, countryCount, versionSets };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<id property="quoteId" column="quote_id" />
|
||||
<result property="quoteKey" column="quote_key" />
|
||||
<result property="customerId" column="customer_id" />
|
||||
<result property="customerCode" column="customer_code" />
|
||||
<result property="productCode" column="product_code" />
|
||||
<result property="productName" column="product_name" />
|
||||
<result property="countryCode" column="country_code" />
|
||||
<result property="countryName" column="country_name" />
|
||||
<result property="destPostcode" column="dest_postcode" />
|
||||
<result property="destPostcode" column="dest_postcode" />
|
||||
<result property="weight" column="weight" />
|
||||
<result property="bizDate" column="biz_date" />
|
||||
<result property="tmsId" column="tms_id" />
|
||||
@@ -46,7 +47,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectPrcQuoteVo">
|
||||
select quote_id, quote_key, customer_id, product_code, product_name,
|
||||
select quote_id, quote_key, customer_id, customer_code, product_code, product_name,
|
||||
country_code, country_name, dest_postcode, weight, biz_date, tms_id,
|
||||
match_source, match_message, price_set_id, price_set_code, price_set_name,
|
||||
set_type, bind_priority, customer_priceset_id, product_country_id,
|
||||
@@ -73,6 +74,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="customerId != null">
|
||||
AND customer_id = #{customerId}
|
||||
</if>
|
||||
<if test="customerCode != null and customerCode != ''">
|
||||
AND customer_code = #{customerCode}
|
||||
</if>
|
||||
<if test="productCode != null and productCode != ''">
|
||||
AND product_code like concat('%', #{productCode}, '%')
|
||||
</if>
|
||||
@@ -103,7 +107,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<insert id="insertPrcQuote" parameterType="PrcQuote">
|
||||
insert into prc_quote (
|
||||
quote_id, quote_key, customer_id, product_code, product_name,
|
||||
quote_id, quote_key, customer_id, customer_code, product_code, product_name,
|
||||
country_code, country_name, dest_postcode, weight, biz_date, tms_id,
|
||||
match_source, match_message, price_set_id, price_set_code, price_set_name,
|
||||
set_type, bind_priority, customer_priceset_id, product_country_id,
|
||||
@@ -112,7 +116,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
freight_amount, surcharge_amount, payable_total, currency, round_mode,
|
||||
create_time
|
||||
) values (
|
||||
#{quoteId}, #{quoteKey}, #{customerId}, #{productCode}, #{productName},
|
||||
#{quoteId}, #{quoteKey}, #{customerId}, #{customerCode}, #{productCode}, #{productName},
|
||||
#{countryCode}, #{countryName}, #{destPostcode}, #{weight}, #{bizDate}, #{tmsId},
|
||||
#{matchSource}, #{matchMessage}, #{priceSetId}, #{priceSetCode}, #{priceSetName},
|
||||
#{setType}, #{bindPriority}, #{customerPricesetId}, #{productCountryId},
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Comment only: version status now includes pending (3)
|
||||
-- 0 draft / 3 pending (effective_time in future) / 1 effect / 2 expired
|
||||
-- No column change; CHAR(1) already supports '3'.
|
||||
|
||||
ALTER TABLE prc_price_set_version
|
||||
MODIFY COLUMN status CHAR(1) NOT NULL DEFAULT '0'
|
||||
COMMENT '0 draft 3 pending 1 effect 2 expired';
|
||||
@@ -0,0 +1,29 @@
|
||||
-- prc_quote add customer_code; backfill from bms_customer
|
||||
-- Idempotent
|
||||
SET NAMES utf8mb4;
|
||||
SET @db := DATABASE();
|
||||
|
||||
SET @has := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_quote' AND COLUMN_NAME = 'customer_code'
|
||||
);
|
||||
SET @sql := IF(@has = 0,
|
||||
'ALTER TABLE prc_quote ADD COLUMN customer_code VARCHAR(50) DEFAULT NULL COMMENT ''customer code'' AFTER customer_id',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @hasIdx := (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_quote' AND INDEX_NAME = 'idx_quote_customer_code'
|
||||
);
|
||||
SET @sql := IF(@hasIdx = 0,
|
||||
'ALTER TABLE prc_quote ADD INDEX idx_quote_customer_code (customer_code, biz_date)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
UPDATE prc_quote q
|
||||
INNER JOIN bms_customer c ON c.customer_id = q.customer_id
|
||||
SET q.customer_code = c.customer_code
|
||||
WHERE q.customer_id IS NOT NULL
|
||||
AND q.customer_id <> 0
|
||||
AND (q.customer_code IS NULL OR q.customer_code = '');
|
||||
@@ -0,0 +1,59 @@
|
||||
-- prc_quote: customer_code required + included in unique business key
|
||||
-- Idempotent; safe after quote_customer_code_alter.sql
|
||||
SET NAMES utf8mb4;
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- Ensure column exists
|
||||
SET @has := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_quote' AND COLUMN_NAME = 'customer_code'
|
||||
);
|
||||
SET @sql := IF(@has = 0,
|
||||
'ALTER TABLE prc_quote ADD COLUMN customer_code VARCHAR(50) DEFAULT NULL COMMENT ''customer code'' AFTER customer_id',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Backfill from bms_customer
|
||||
UPDATE prc_quote q
|
||||
INNER JOIN bms_customer c ON c.customer_id = q.customer_id
|
||||
SET q.customer_code = c.customer_code
|
||||
WHERE q.customer_id IS NOT NULL
|
||||
AND q.customer_id <> 0
|
||||
AND (q.customer_code IS NULL OR q.customer_code = '');
|
||||
|
||||
-- Remove rows that still have no customer_code (cannot join unique NOT NULL)
|
||||
DELETE FROM prc_quote WHERE customer_code IS NULL OR customer_code = '';
|
||||
|
||||
-- NOT NULL
|
||||
ALTER TABLE prc_quote
|
||||
MODIFY COLUMN customer_code VARCHAR(50) NOT NULL COMMENT 'customer code';
|
||||
|
||||
-- Drop old non-unique index if present
|
||||
SET @hasIdx := (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_quote' AND INDEX_NAME = 'idx_quote_customer_code'
|
||||
);
|
||||
SET @sql := IF(@hasIdx > 0,
|
||||
'ALTER TABLE prc_quote DROP INDEX idx_quote_customer_code',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Normalized postcode for unique (NULL -> '')
|
||||
SET @hasGen := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_quote' AND COLUMN_NAME = 'quote_postcode_key'
|
||||
);
|
||||
SET @sql := IF(@hasGen = 0,
|
||||
'ALTER TABLE prc_quote ADD COLUMN quote_postcode_key VARCHAR(20) AS (IFNULL(dest_postcode, '''')) STORED COMMENT ''dest_postcode for unique key'' AFTER dest_postcode',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Unique: customer_code + product + country + postcode + weight + biz_date
|
||||
SET @hasUk := (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_quote' AND INDEX_NAME = 'uk_quote_cust_biz'
|
||||
);
|
||||
SET @sql := IF(@hasUk = 0,
|
||||
'ALTER TABLE prc_quote ADD UNIQUE KEY uk_quote_cust_biz (customer_code, product_code, country_code, quote_postcode_key, weight, biz_date)',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@@ -10,10 +10,13 @@ CREATE TABLE IF NOT EXISTS prc_quote (
|
||||
quote_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '试算ID',
|
||||
quote_key CHAR(32) NOT NULL COMMENT '试算指纹 MD5(客户|产品|国家|重量|计费日期)',
|
||||
customer_id BIGINT(20) NOT NULL COMMENT '客户ID',
|
||||
customer_code VARCHAR(50) NOT NULL COMMENT '客户代码',
|
||||
product_code VARCHAR(20) NOT NULL COMMENT '产品代码',
|
||||
product_name VARCHAR(100) DEFAULT NULL COMMENT '产品名称',
|
||||
country_code CHAR(2) DEFAULT NULL COMMENT '国家二字码',
|
||||
country_name VARCHAR(50) DEFAULT NULL COMMENT '国家名称',
|
||||
dest_postcode VARCHAR(20) DEFAULT NULL COMMENT '目的邮编',
|
||||
quote_postcode_key VARCHAR(20) AS (IFNULL(dest_postcode, '')) STORED COMMENT '邮编唯一键辅助',
|
||||
weight DECIMAL(10,3) NOT NULL COMMENT '计费重量KG',
|
||||
biz_date DATE NOT NULL COMMENT '计费日期',
|
||||
tms_id BIGINT(20) DEFAULT NULL COMMENT '多租户系统ID',
|
||||
@@ -46,6 +49,7 @@ CREATE TABLE IF NOT EXISTS prc_quote (
|
||||
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (quote_id),
|
||||
UNIQUE KEY uk_quote_key (quote_key),
|
||||
UNIQUE KEY uk_quote_cust_biz (customer_code, product_code, country_code, quote_postcode_key, weight, biz_date),
|
||||
INDEX idx_quote_customer (customer_id, biz_date),
|
||||
INDEX idx_quote_product (product_code, country_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='计费试算主表';
|
||||
|
||||
@@ -113,10 +113,11 @@ export function rejectPriceSet(priceSetId) {
|
||||
})
|
||||
}
|
||||
|
||||
export function activatePriceSetVersion(psvId) {
|
||||
export function activatePriceSetVersion(psvId, data) {
|
||||
return request({
|
||||
url: '/price/set/version/activate/' + psvId,
|
||||
method: 'put'
|
||||
method: 'put',
|
||||
data: data || {}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 立即刷新 bms_product / bms_product_country 的 status 字段
|
||||
*
|
||||
* 日常由后端定时任务 BmsStatusRefreshTask 每天 02:00 自动跑,
|
||||
* 此接口用于运维/测试手动触发。返回 success msg 形如:
|
||||
* 「状态刷新完成:bms_product 更新 X 行,bms_product_country 更新 Y 行」
|
||||
*/
|
||||
export function refreshStatus() {
|
||||
return request({
|
||||
url: '/price/status/refresh',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
@@ -8,39 +8,23 @@
|
||||
title="Excel 导入:相同产品代码+生效日期不覆盖;不同生效日期新增一条产品。同时入库国家×重量段(运费/挂号费/尺寸/尾程/时效)。"
|
||||
/>
|
||||
|
||||
<el-tabs v-model="effectTab" class="mb8" @tab-change="onTabChange">
|
||||
<el-tab-pane
|
||||
v-for="t in statusTabs"
|
||||
:key="t.name"
|
||||
:label="t.label"
|
||||
:name="t.name"
|
||||
/>
|
||||
</el-tabs>
|
||||
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:search-columns="searchColumns"
|
||||
:query="queryList"
|
||||
:extra-params="extraParams"
|
||||
row-key="productId"
|
||||
>
|
||||
<template #toolbar="{ selection, ids }">
|
||||
<template #toolbar="{ selection }">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['price:product:add']">新建产品</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" plain icon="Edit" :disabled="selection.length !== 1 || (selection[0] && isProductActive(selection[0]))" @click="handleUpdate(selection[0])" v-hasPermi="['price:product:edit']">修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="Delete" :disabled="!selection.length || selection.some(isProductActive)" @click="handleDelete(ids)" v-hasPermi="['price:product:remove']">删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['price:product:import']">导入 Excel</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="Refresh" @click="handleRefreshStatus" v-hasPermi="['price:product:edit']">刷新状态</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<template #col-cargoAttr="{ row }">
|
||||
@@ -55,7 +39,6 @@
|
||||
<el-button link type="primary" icon="View" @click="countryRef.open(row)" v-hasPermi="['price:product:query']">产品明细</el-button>
|
||||
<el-button link type="primary" icon="Money" @click="surchargeRef.open(row)" v-hasPermi="['price:surcharge:query']">附加费</el-button>
|
||||
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '已生效不可编辑' : ''" link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
|
||||
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '已生效不可删除' : ''" link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
@@ -78,8 +61,7 @@ import ExcelImportDialog from '@/components/ExcelImportDialog'
|
||||
import ProductFormDialog from './components/ProductFormDialog.vue'
|
||||
import ProductCountryDrawer from './components/ProductCountryDrawer.vue'
|
||||
import ProductSurchargeDrawer from './components/ProductSurchargeDrawer.vue'
|
||||
import { listProduct, delProduct } from '@/api/price/product'
|
||||
import { refreshStatus } from '@/api/price/status'
|
||||
import { listProduct } from '@/api/price/product'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_product_effect_status } = useDict('bms_product_effect_status')
|
||||
@@ -90,41 +72,6 @@ const countryRef = ref()
|
||||
const surchargeRef = ref()
|
||||
const importRef = ref()
|
||||
|
||||
const TAB_ACTIVE = 'active'
|
||||
const TAB_PENDING = 'pending'
|
||||
const TAB_EXPIRED = 'expired'
|
||||
|
||||
const effectTab = ref(TAB_ACTIVE)
|
||||
|
||||
const statusTabs = computed(() => [
|
||||
{ name: TAB_ACTIVE, code: '1', label: dictLabel('1', 'ACTIVE') },
|
||||
{ name: TAB_PENDING, code: '0', label: dictLabel('0', 'PENDING') },
|
||||
{ name: TAB_EXPIRED, code: '2', label: dictLabel('2', 'EXPIRED') }
|
||||
])
|
||||
|
||||
const extraParams = computed(() => ({ status: tabToStatus(effectTab.value) }))
|
||||
|
||||
function dictLabel(code, legacy) {
|
||||
const list = bms_product_effect_status.value || []
|
||||
const hit = list.find(d => String(d.value) === code || String(d.value) === legacy)
|
||||
return hit?.label || code
|
||||
}
|
||||
|
||||
function tabToStatus(name) {
|
||||
if (name === TAB_ACTIVE || name === 'ACTIVE' || name === '1') return '1'
|
||||
if (name === TAB_PENDING || name === 'PENDING' || name === '0') return '0'
|
||||
if (name === TAB_EXPIRED || name === 'EXPIRED' || name === '2') return '2'
|
||||
return '1'
|
||||
}
|
||||
|
||||
function onTabChange(name) {
|
||||
effectTab.value = String(name || TAB_ACTIVE)
|
||||
nextTick(() => {
|
||||
tableRef.value?.clearSelection()
|
||||
tableRef.value?.reload()
|
||||
})
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ prop: 'productCode', label: '产品代码', minWidth: 110 },
|
||||
{ prop: 'productName', label: '产品名称', minWidth: 140, showOverflowTooltip: true },
|
||||
@@ -138,7 +85,7 @@ const columns = [
|
||||
{ prop: 'surchargeCount', label: '附加费数', width: 90, align: 'center' },
|
||||
{ prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' },
|
||||
{ prop: 'updateNote', label: '更新内容', minWidth: 160, showOverflowTooltip: true },
|
||||
{ prop: 'action', label: '操作', width: 300, fixed: 'right', slot: 'action' }
|
||||
{ prop: 'action', label: '操作', width: 240, fixed: 'right', slot: 'action' }
|
||||
]
|
||||
|
||||
const searchColumns = [
|
||||
@@ -155,7 +102,7 @@ const searchColumns = [
|
||||
]
|
||||
|
||||
function queryList(params) {
|
||||
return listProduct({ ...params, status: tabToStatus(effectTab.value) })
|
||||
return listProduct(params)
|
||||
}
|
||||
|
||||
function isProductActive(row) {
|
||||
@@ -174,32 +121,9 @@ function handleUpdate(row) {
|
||||
formRef.value.open(row)
|
||||
}
|
||||
|
||||
function handleDelete(ids) {
|
||||
const idText = Array.isArray(ids) ? ids.join(',') : ids
|
||||
proxy.$modal.confirm('是否确认删除所选产品?国家明细将一并删除。').then(() => {
|
||||
return delProduct(idText)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('删除成功')
|
||||
tableRef.value.refresh()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function handleImport() {
|
||||
importRef.value.open()
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发 bms_product / bms_product_country 状态刷新
|
||||
* 日常由后端定时任务每天 02:00 自动跑;此处用于运维即时刷新
|
||||
*/
|
||||
function handleRefreshStatus() {
|
||||
proxy.$modal.confirm('将立即按 effective_date / expire_date 重新计算所有产品及国家明细的状态,是否继续?').then(() => {
|
||||
return refreshStatus()
|
||||
}).then(res => {
|
||||
proxy.$modal.msgSuccess(res.msg || '刷新成功')
|
||||
tableRef.value && tableRef.value.refresh()
|
||||
}).catch(() => {})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
<el-alert v-else-if="errorMsg" :title="errorMsg" type="error" show-icon :closable="false" />
|
||||
<div v-else class="quote-result">
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="客户代码">{{ result.customerCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户ID">{{ result.customerId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="产品">{{ result.productName || '-' }} ({{ result.productCode }})</el-descriptions-item>
|
||||
<el-descriptions-item label="国家">{{ result.countryName || '-' }} ({{ result.countryCode }})</el-descriptions-item>
|
||||
@@ -189,6 +190,7 @@
|
||||
<el-empty v-if="!viewResult" description="暂无详情" />
|
||||
<div v-else class="quote-result">
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="客户代码">{{ viewResult.customerCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户ID">{{ viewResult.customerId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="产品">{{ viewResult.productName || '-' }} ({{ viewResult.productCode }})</el-descriptions-item>
|
||||
<el-descriptions-item label="国家">{{ viewResult.countryName || '-' }} ({{ viewResult.countryCode }})</el-descriptions-item>
|
||||
@@ -259,6 +261,7 @@ const historyLoading = ref(false)
|
||||
const viewResult = ref(null)
|
||||
|
||||
const historyColumns = [
|
||||
{ label: '客户代码', prop: 'customerCode', minWidth: 120, showOverflowTooltip: true },
|
||||
{ label: '客户ID', prop: 'customerId', align: 'center', width: 90 },
|
||||
{ label: '产品', prop: 'product', minWidth: 160, slot: 'product' },
|
||||
{ label: '国家', prop: 'country', minWidth: 130, slot: 'country' },
|
||||
@@ -272,6 +275,7 @@ const historyColumns = [
|
||||
]
|
||||
|
||||
const historySearchColumns = [
|
||||
{ label: '客户代码', prop: 'customerCode', component: 'input', placeholder: '客户代码', width: '150px' },
|
||||
{ label: '客户ID', prop: 'customerId', component: 'input', placeholder: '客户ID', width: '140px' },
|
||||
{ label: '产品代码', prop: 'productCode', component: 'input', placeholder: '产品代码', width: '150px' },
|
||||
{ label: '国家代码', prop: 'countryCode', component: 'input', placeholder: '如 DE', width: '120px' },
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<el-tabs v-model="statusTab" class="ver-tabs">
|
||||
<el-tab-pane :label="tabLabel('all')" name="all" />
|
||||
<el-tab-pane :label="tabLabel('0')" name="0" />
|
||||
<el-tab-pane :label="tabLabel('3')" name="3" />
|
||||
<el-tab-pane :label="tabLabel('1')" name="1" />
|
||||
<el-tab-pane :label="tabLabel('2')" name="2" />
|
||||
</el-tabs>
|
||||
@@ -31,28 +32,28 @@
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetails(row)">查看详情</el-button>
|
||||
<el-button
|
||||
v-if="String(row.status) === '0'"
|
||||
v-if="isPriceEditableVersion(row)"
|
||||
link
|
||||
type="primary"
|
||||
@click="emit('edit', row)"
|
||||
v-hasPermi="['price:set:edit']"
|
||||
>继续配置</el-button>
|
||||
<el-button
|
||||
v-if="String(row.status) === '0'"
|
||||
v-if="isDraftVersion(row)"
|
||||
link
|
||||
type="success"
|
||||
@click="emit('activate', row)"
|
||||
v-hasPermi="['price:set:edit']"
|
||||
>启动</el-button>
|
||||
<el-button
|
||||
v-if="String(row.status) === '0'"
|
||||
v-if="isDraftVersion(row)"
|
||||
link
|
||||
type="danger"
|
||||
@click="emit('remove', row)"
|
||||
v-hasPermi="['price:set:edit']"
|
||||
>删除</el-button>
|
||||
<el-button
|
||||
v-if="String(row.status) === '1'"
|
||||
v-if="String(row.status) === '1' && !hasDraft"
|
||||
link
|
||||
type="primary"
|
||||
@click="emit('copy-version', row)"
|
||||
@@ -66,7 +67,7 @@
|
||||
|
||||
<script setup name="SetVersionTable">
|
||||
import SetSectionCard from './SetSectionCard.vue'
|
||||
import { versionStatusLabel, versionStatusTag } from '../../utils/priceSet'
|
||||
import { versionStatusLabel, versionStatusTag, isDraftVersion, isPriceEditableVersion } from '../../utils/priceSet'
|
||||
|
||||
const props = defineProps({
|
||||
versions: { type: Array, default: () => [] },
|
||||
@@ -84,7 +85,7 @@ const colAction = '操作'
|
||||
const statusTab = ref('all')
|
||||
|
||||
const counts = computed(() => {
|
||||
const map = { all: 0, '0': 0, '1': 0, '2': 0 }
|
||||
const map = { all: 0, '0': 0, '3': 0, '1': 0, '2': 0 }
|
||||
;(props.versions || []).forEach(v => {
|
||||
map.all++
|
||||
const s = String(v.status == null ? '' : v.status)
|
||||
@@ -97,12 +98,15 @@ function tabLabel(key) {
|
||||
const labels = {
|
||||
all: '全部',
|
||||
'0': '草稿',
|
||||
'3': '待生效',
|
||||
'1': '生效',
|
||||
'2': '失效'
|
||||
}
|
||||
return labels[key] + ' (' + (counts.value[key] || 0) + ')'
|
||||
}
|
||||
|
||||
const hasDraft = computed(() => (props.versions || []).some(v => isDraftVersion(v)))
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
const list = props.versions || []
|
||||
if (statusTab.value === 'all') return list
|
||||
|
||||
@@ -35,6 +35,34 @@
|
||||
:version="detailVersion"
|
||||
:set-type="set.setType"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="activateVisible"
|
||||
:title="activateTitle"
|
||||
width="460px"
|
||||
append-to-body
|
||||
@closed="onActivateClosed"
|
||||
>
|
||||
<el-form ref="activateFormRef" :model="activateForm" :rules="activateRules" label-width="100px">
|
||||
<el-form-item label="版本号">
|
||||
<span>{{ activateForm.versionNo || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="生效时间" prop="effectiveTime">
|
||||
<el-date-picker
|
||||
v-model="activateForm.effectiveTime"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
:placeholder="activatePh"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="activate-hint">{{ activateHint }}</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="activateVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="activateSubmitting" @click="submitActivate">确认启动</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -92,13 +120,59 @@ function confirmAction(text, request, successText, done) {
|
||||
function onDisable() {
|
||||
confirmAction('停用后不再参与计费命中,确认停用?', () => disablePriceSet(set.value.priceSetId), '已停用')
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
const activateVisible = ref(false)
|
||||
const activateSubmitting = ref(false)
|
||||
const activateFormRef = ref()
|
||||
const activateTitle = '启动版本'
|
||||
const activatePh = '请选择生效时间'
|
||||
const activateHint = '若生效时间晚于当前,版本将进入「待生效」;到时自动生效并使上一版本失效。'
|
||||
const activateForm = reactive({
|
||||
psvId: undefined,
|
||||
versionNo: '',
|
||||
effectiveTime: undefined
|
||||
})
|
||||
const activateRules = {
|
||||
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function onActivateVersion(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
confirmAction(
|
||||
'确认启动版本 ' + (row.versionNo || '') + '?启动后将以当前时间生效,上一版本自动失效。',
|
||||
() => activatePriceSetVersion(row.psvId),
|
||||
'版本已启动'
|
||||
)
|
||||
activateForm.psvId = row.psvId
|
||||
activateForm.versionNo = row.versionNo || ''
|
||||
activateForm.effectiveTime = formatNow()
|
||||
activateVisible.value = true
|
||||
}
|
||||
|
||||
function onActivateClosed() {
|
||||
activateForm.psvId = undefined
|
||||
activateForm.versionNo = ''
|
||||
activateForm.effectiveTime = undefined
|
||||
activateFormRef.value && activateFormRef.value.clearValidate()
|
||||
}
|
||||
|
||||
function submitActivate() {
|
||||
activateFormRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
activateSubmitting.value = true
|
||||
activatePriceSetVersion(activateForm.psvId, { effectiveTime: activateForm.effectiveTime })
|
||||
.then(() => {
|
||||
const eff = activateForm.effectiveTime
|
||||
const pending = eff && new Date(eff.replace(/-/g, '/')).getTime() > Date.now()
|
||||
proxy.$modal.msgSuccess(pending ? '已设为待生效' : '版本已启动')
|
||||
activateVisible.value = false
|
||||
load()
|
||||
})
|
||||
.finally(() => {
|
||||
activateSubmitting.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
function onCopy() {
|
||||
copyPriceSet(set.value.priceSetId).then(res => {
|
||||
@@ -108,6 +182,11 @@ function onCopy() {
|
||||
}
|
||||
function onCopyVersion(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
const hasDraft = (set.value.versions || []).some(v => v && String(v.status) === '0')
|
||||
if (hasDraft) {
|
||||
proxy.$modal.msgWarning('已有草稿版本,请先启动或删除后再复制')
|
||||
return
|
||||
}
|
||||
proxy.$modal.confirm(
|
||||
'确认复制版本 ' + (row.versionNo || '') + '为新草稿?将新增版本并拷贝全部明细。'
|
||||
).then(() => copyPriceSetVersion(row.psvId)).then(res => {
|
||||
@@ -153,3 +232,12 @@ onMounted(() => {
|
||||
load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.activate-hint {
|
||||
margin: -4px 0 0 100px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,23 +40,43 @@ const STATUS_TAG = {
|
||||
/** 默认取值方式 */
|
||||
export const DEFAULT_ROUND_MODE = 'HALF_UP'
|
||||
|
||||
/** version status: 0 draft / 1 active / 2 expired */
|
||||
/** version status: 0 draft / 3 pending / 1 active / 2 expired */
|
||||
export const VER_STATUS = {
|
||||
DRAFT: '0',
|
||||
PENDING: '3',
|
||||
EFFECT: '1',
|
||||
EXPIRED: '2'
|
||||
}
|
||||
|
||||
export function versionStatusLabel(status) {
|
||||
const map = { '0': '草稿', '1': '生效', '2': '失效' }
|
||||
const map = {
|
||||
'0': '草稿',
|
||||
'3': '待生效',
|
||||
'1': '生效',
|
||||
'2': '失效'
|
||||
}
|
||||
return map[status] || status || '-'
|
||||
}
|
||||
|
||||
export function versionStatusTag(status) {
|
||||
const map = { '0': 'warning', '1': 'success', '2': 'info' }
|
||||
const map = { '0': 'warning', '3': 'primary', '1': 'success', '2': 'info' }
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
/** True draft only (not pending) */
|
||||
export function isDraftVersion(row) {
|
||||
return !!row && String(row.status) === VER_STATUS.DRAFT
|
||||
}
|
||||
|
||||
export function isPendingVersion(row) {
|
||||
return !!row && String(row.status) === VER_STATUS.PENDING
|
||||
}
|
||||
|
||||
/** Draft or pending: may continue editing price details */
|
||||
export function isPriceEditableVersion(row) {
|
||||
return isDraftVersion(row) || isPendingVersion(row)
|
||||
}
|
||||
|
||||
export function nextVersionNo(versions) {
|
||||
const seqs = (versions || []).map(v => Number(v.versionSeq) || 0)
|
||||
const max = seqs.length ? Math.max.apply(null, seqs) : 0
|
||||
|
||||
Reference in New Issue
Block a user