From f3ef2df949a4f845345669c2b3ca98e31575543f Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Thu, 17 Sep 2026 14:42:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A5=97=E9=A4=90=E5=90=AF=E7=94=A8=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E7=94=9F=E6=95=88=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/api/PriceSyncEsLogAdvice.java | 60 ++++-- .../controller/price/PriceSetController.java | 8 +- .../price/PriceStatusController.java | 40 ---- .../main/resources/i18n/messages.properties | 5 +- .../price/domain/PrcPriceSetVersion.java | 2 +- .../java/com/ruoyi/price/domain/PrcQuote.java | 2 + .../price/domain/vo/TmsPriceQuoteRequest.java | 2 + .../ruoyi/price/search/EsBizLogSupport.java | 203 +++++++++++++++++- .../price/service/IPrcPriceSetService.java | 10 +- .../service/impl/PrcOrderBillServiceImpl.java | 17 +- .../service/impl/PrcPriceSetServiceImpl.java | 131 ++++++----- .../impl/TmsPriceQuoteServiceImpl.java | 69 +++++- .../price/task/BmsStatusRefreshTask.java | 63 ++---- .../resources/mapper/price/PrcQuoteMapper.xml | 12 +- ...ice_set_version_status_pending_comment.sql | 7 + .../sql/price/quote_customer_code_alter.sql | 29 +++ .../quote_customer_code_unique_alter.sql | 59 +++++ bms-back/sql/price/quote_fee.sql | 4 + bms-front/src/api/price/set.js | 5 +- bms-front/src/api/price/status.js | 15 -- .../src/views/price/basic/product/index.vue | 84 +------- bms-front/src/views/price/billing/quote.vue | 4 + .../set/components/detail/SetVersionTable.vue | 16 +- bms-front/src/views/price/set/detail.vue | 98 ++++++++- .../src/views/price/set/utils/priceSet.js | 26 ++- 25 files changed, 684 insertions(+), 287 deletions(-) delete mode 100644 bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/PriceStatusController.java create mode 100644 bms-back/sql/price/price_set_version_status_pending_comment.sql create mode 100644 bms-back/sql/price/quote_customer_code_alter.sql create mode 100644 bms-back/sql/price/quote_customer_code_unique_alter.sql delete mode 100644 bms-front/src/api/price/status.js diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/PriceSyncEsLogAdvice.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/PriceSyncEsLogAdvice.java index 5bdc28fc..21af01ba 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/PriceSyncEsLogAdvice.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/PriceSyncEsLogAdvice.java @@ -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); } } diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/PriceSetController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/PriceSetController.java index 25c9895f..043f1b3a 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/PriceSetController.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/PriceSetController.java @@ -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')") diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/PriceStatusController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/PriceStatusController.java deleted file mode 100644 index 8e9224a5..00000000 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/PriceStatusController.java +++ /dev/null @@ -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; - -/** - * 状态自动刷新 - 手动触发入口 - * - *

日常由 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); - } -} \ No newline at end of file diff --git a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties index a48b5b97..f4b3b054 100644 --- a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -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}条) diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcPriceSetVersion.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcPriceSetVersion.java index b0d64122..8a8b965e 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcPriceSetVersion.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcPriceSetVersion.java @@ -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 details; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuote.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuote.java index 815fd1c1..52101e60 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuote.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuote.java @@ -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; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteRequest.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteRequest.java index 163553fe..98466e5c 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteRequest.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteRequest.java @@ -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") diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsBizLogSupport.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsBizLogSupport.java index f3d4daf8..df503e28 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsBizLogSupport.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsBizLogSupport.java @@ -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) diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcPriceSetService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcPriceSetService.java index 57c698eb..9c4ecab1 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcPriceSetService.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcPriceSetService.java @@ -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 */ diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java index 3b434c70..092a5b46 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java @@ -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()); diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java index 15b3376f..55260142 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java @@ -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 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 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() { diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java index ce0e0750..f5ff5603 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java @@ -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 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())) { diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/task/BmsStatusRefreshTask.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/task/BmsStatusRefreshTask.java index 1481e79c..bf17bd87 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/task/BmsStatusRefreshTask.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/task/BmsStatusRefreshTask.java @@ -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 状态自动刷新 - * - *

每天 02:00 根据 effective_date / expire_date 与当天日期,回填 status: - *

- * - *

对应字典 bms_product_effect_status 的 value=ACTIVE/PENDING/EXPIRED。 - * 实际存储使用 0/1/2 与列 comment 保持一致;前端 dict-tag 需做 value 映射或调整 dict_value。 - * - *

手动触发:调用 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 }; - } -} \ No newline at end of file +} diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteMapper.xml index e267bf7d..3bf96283 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteMapper.xml @@ -8,11 +8,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + - + @@ -46,7 +47,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - 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" AND customer_id = #{customerId} + + AND customer_code = #{customerCode} + AND product_code like concat('%', #{productCode}, '%') @@ -103,7 +107,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 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}, diff --git a/bms-back/sql/price/price_set_version_status_pending_comment.sql b/bms-back/sql/price/price_set_version_status_pending_comment.sql new file mode 100644 index 00000000..87f8c87a --- /dev/null +++ b/bms-back/sql/price/price_set_version_status_pending_comment.sql @@ -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'; diff --git a/bms-back/sql/price/quote_customer_code_alter.sql b/bms-back/sql/price/quote_customer_code_alter.sql new file mode 100644 index 00000000..4837cec4 --- /dev/null +++ b/bms-back/sql/price/quote_customer_code_alter.sql @@ -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 = ''); diff --git a/bms-back/sql/price/quote_customer_code_unique_alter.sql b/bms-back/sql/price/quote_customer_code_unique_alter.sql new file mode 100644 index 00000000..6cc0b76f --- /dev/null +++ b/bms-back/sql/price/quote_customer_code_unique_alter.sql @@ -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; diff --git a/bms-back/sql/price/quote_fee.sql b/bms-back/sql/price/quote_fee.sql index 8a6937e6..f2942fea 100644 --- a/bms-back/sql/price/quote_fee.sql +++ b/bms-back/sql/price/quote_fee.sql @@ -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='计费试算主表'; diff --git a/bms-front/src/api/price/set.js b/bms-front/src/api/price/set.js index 3507a847..ff8b31bf 100644 --- a/bms-front/src/api/price/set.js +++ b/bms-front/src/api/price/set.js @@ -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 || {} }) } diff --git a/bms-front/src/api/price/status.js b/bms-front/src/api/price/status.js deleted file mode 100644 index a83a3423..00000000 --- a/bms-front/src/api/price/status.js +++ /dev/null @@ -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' - }) -} \ No newline at end of file diff --git a/bms-front/src/views/price/basic/product/index.vue b/bms-front/src/views/price/basic/product/index.vue index 91434007..9ad86242 100644 --- a/bms-front/src/views/price/basic/product/index.vue +++ b/bms-front/src/views/price/basic/product/index.vue @@ -8,39 +8,23 @@ title="Excel 导入:相同产品代码+生效日期不覆盖;不同生效日期新增一条产品。同时入库国家×重量段(运费/挂号费/尺寸/尾程/时效)。" /> - - - - -