From 998aac74c875ab110b6df8685fb6159b92a8cf9e Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Wed, 30 Sep 2026 11:30:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=AF=BC=E5=85=A5=E5=A5=97?= =?UTF-8?q?=E9=A4=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/resources/i18n/messages.properties | 1 + .../com/bms/price/domain/PrcPriceSet.java | 2 +- .../service/impl/PrcPriceSetServiceImpl.java | 54 ++++--- ...rc_price_set_detail_product_code_alter.sql | 3 + .../set/components/create/PriceEditPanel.vue | 1 + .../set/components/create/StepDetails.vue | 152 +++++++----------- bms-front/src/views/price/set/create.vue | 33 +++- 7 files changed, 132 insertions(+), 114 deletions(-) create mode 100644 bms-back/sql/price/prc_price_set_detail_product_code_alter.sql diff --git a/bms-back/bms-admin/src/main/resources/i18n/messages.properties b/bms-back/bms-admin/src/main/resources/i18n/messages.properties index a9d029e2..ab0b574e 100644 --- a/bms-back/bms-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/bms-admin/src/main/resources/i18n/messages.properties @@ -78,6 +78,7 @@ price.set.import.fixedOnly=\u4ec5\u56fa\u5b9a\u4ef7\u5957\u9910\u652f\u6301\u5bf price.set.import.noDetails=\u5957\u9910\u6682\u65e0\u4ef7\u683c\u660e\u7ec6\uff0c\u8bf7\u5148\u62c9\u53d6\u4ea7\u54c1\u4ef7 price.set.import.noPriceRows=\u4ef7\u8868\u672a\u89e3\u6790\u5230\u6709\u6548\u4ef7\u683c\u884c price.set.import.success=\u5bfc\u5165\u5b8c\u6210\uff1a\u66f4\u65b0 {0} \u6761\uff0c\u672a\u5339\u914d {1} \u6761\uff0c\u5957\u9910\u65e0\u5bf9\u5e94\u4ea7\u54c1 {2} \u4e2a +price.set.import.noDetailWritten=\u672a\u5199\u5165\u4efb\u4f55\u5957\u9910\u660e\u7ec6\uff08\u4ef7\u8868\u65e0\u53ef\u7528\u91cd\u91cf\u6bb5\u6216\u5747\u4e3a\u7a7a\u8fd0\u8d39/\u6302\u53f7\u8d39\u4e14\u65e0\u53ef\u66f4\u65b0\u884c\uff09 price.set.import.upsertSuccess=\u5bfc\u5165\u5b8c\u6210\uff1a\u65b0\u589e {0} \u6761\uff0c\u66f4\u65b0 {1} \u6761\uff0c\u8df3\u8fc7\u7a7a\u8fd0\u8d39/\u6302\u53f7\u8d39 {2} \u6761 price.set.pull.success=\u62c9\u53d6\u5b8c\u6210\uff1a\u5df2\u5199\u5165 {0} \u6761\u5957\u9910\u660e\u7ec6 price.set.customer.notExists=客户绑定不存在 diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPriceSet.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPriceSet.java index e5d4bd43..661693b3 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPriceSet.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPriceSet.java @@ -44,7 +44,7 @@ public class PrcPriceSet extends BaseEntity private List customers; /** async save task id (not persisted) */ private String saveTaskId; - /** Y: backend joins product library into details (skip front pull) */ + /** Y: ignored on save; library pull only via /detail/pullFromLibrary */ private String pullFromLibrary; /** Y: even if version already has details, delete+re-pull from library */ private String forcePullLibrary; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceSetServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceSetServiceImpl.java index cda30ada..c6f57b97 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceSetServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceSetServiceImpl.java @@ -892,7 +892,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService set.setUpdateBy(operName); set.setCreateBy(operName); set.setSaveTaskId(taskId); - set.setPullFromLibrary("Y"); + // Dedicated pull API — does not go through savePrcPriceSet. + set.setPullFromLibrary(null); set.setForcePullLibrary(force ? "Y" : "N"); if (psvId == null) { @@ -1004,6 +1005,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService List hits = matchImportDetails(candidates, row); if (!hits.isEmpty()) { + // Existing band: only skip when Excel left both fees blank (keep old prices). if (freight == null && registration == null) { skippedFee++; @@ -1029,11 +1031,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService } continue; } - if (freight == null && registration == null) - { - skippedFee++; - continue; - } + // New band: always insert weight/country row (fees may be null; editable later). PrcPriceSetDetail created = buildDetailFromImportRow( priceSetId, psvId, productCode, productName, row, freight, registration); created.setPspId(pspId); @@ -1043,11 +1041,19 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { touched.add(created.getPsdId()); } + if (freight == null && registration == null) + { + skippedFee++; + } inserted++; } } setProductMapper.bindDetailPspByPsvId(psvId); reportImportProgress(taskId, priceSetId, 95, "finishing"); + if (inserted == 0 && updated == 0) + { + throw new ServiceException(MessageUtils.message("price.set.import.noDetailWritten")); + } return MessageUtils.message("price.set.import.upsertSuccess", Integer.valueOf(inserted), Integer.valueOf(updated), Integer.valueOf(skippedFee)); } @@ -1086,16 +1092,16 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService PrcPriceSetDetail d = new PrcPriceSetDetail(); d.setPriceSetId(priceSetId); d.setPsvId(psvId); - d.setProductCode(productCode); - d.setProductName(productName); + d.setProductCode(trimToLen(productCode, 20)); + d.setProductName(trimToLen(productName, 50)); d.setCountryCode(row.getCountryCode()); - d.setCountryName(row.getCountryName()); + d.setCountryName(trimToLen(row.getCountryName(), 50)); d.setZoneId(row.getZoneId()); d.setZoneCode(row.getZoneCode()); - d.setZoneName(row.getZoneName()); + d.setZoneName(trimToLen(row.getZoneName(), 100)); d.setWeightMin(row.getWeightMin()); d.setWeightMax(row.getWeightMax()); - d.setWeightLabel(formatWeightLabel(row.getWeightMin(), row.getWeightMax())); + d.setWeightLabel(trimToLen(formatWeightLabel(row.getWeightMin(), row.getWeightMax()), 50)); d.setProductCountryId(row.getDetailId()); d.setFvValue(freight); d.setFvRegistrationFee(registration); @@ -1105,6 +1111,20 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService return d; } + private static String trimToLen(String s, int max) + { + if (s == null) + { + return null; + } + String t = s.trim(); + if (t.length() <= max) + { + return t; + } + return t.substring(0, max); + } + private void reportImportProgress(String taskId, Long priceSetId, int percent, String message) { if (StringUtils.isEmpty(taskId) || saveAsyncRunner == null) @@ -2136,12 +2156,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService private void saveOrUpdateVersion(PrcPriceSet set) { PrcPriceSetVersion version = resolveSaveVersion(set); - boolean pullLibrary = "Y".equalsIgnoreCase(set.getPullFromLibrary()); - // null details = leave existing rows alone; empty list = clear / replace with nothing + // null details = leave existing rows alone; empty list = clear / replace with nothing. + // Library pull must NOT run here — only via pullFromLibrary() / Excel import on Step 2. boolean detailsProvided = set.getDetails() != null; List details = filterDetails(set.getDetails()); // Allow creating an empty draft version when client sends version (Step1 -> Step2). - if (version == null && !detailsProvided && !pullLibrary) + if (version == null && !detailsProvided) { return; } @@ -2192,11 +2212,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService versionMapper.updatePrcPriceSetVersion(version); } set.setVersion(version); - if (pullLibrary) - { - replaceVersionDetailsFromLibrary(set, version); - } - else if (detailsProvided) + if (detailsProvided) { upsertVersionDetails(set, version, details); reportSaveProgress(set, 85, "saving surcharges"); diff --git a/bms-back/sql/price/prc_price_set_detail_product_code_alter.sql b/bms-back/sql/price/prc_price_set_detail_product_code_alter.sql new file mode 100644 index 00000000..0a595ccb --- /dev/null +++ b/bms-back/sql/price/prc_price_set_detail_product_code_alter.sql @@ -0,0 +1,3 @@ +-- Widen prc_price_set_detail.product_code to match product / set_product (varchar 20). +ALTER TABLE prc_price_set_detail + MODIFY COLUMN product_code varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品代码'; diff --git a/bms-front/src/views/price/set/components/create/PriceEditPanel.vue b/bms-front/src/views/price/set/components/create/PriceEditPanel.vue index 9915d262..740d7080 100644 --- a/bms-front/src/views/price/set/components/create/PriceEditPanel.vue +++ b/bms-front/src/views/price/set/components/create/PriceEditPanel.vue @@ -22,6 +22,7 @@ :products="products" :price-set-id="priceSetId" :psv-id="version.psvId" + @update:psv-id="v => { if (v != null && v !== '') version.psvId = v }" :set-type="setType" :effective-time="version.effectiveTime" :version-no="version.versionNo" diff --git a/bms-front/src/views/price/set/components/create/StepDetails.vue b/bms-front/src/views/price/set/components/create/StepDetails.vue index 7631eb3a..4a7205d4 100644 --- a/bms-front/src/views/price/set/components/create/StepDetails.vue +++ b/bms-front/src/views/price/set/components/create/StepDetails.vue @@ -6,34 +6,19 @@ {{ headerTitle }} {{ typeHint }} +
+ 拉取产品列表 + 导入 Excel 建价 +
-
-

暂无套餐销售价,请选择一种建价方式

-
- - -
-
- - - - '· 当前:' + setTypeLabel(props.setType)) +const emptyTip = '暂无产品,请先拉取产品列表或导入 Excel 建价' -/** Show pull/import when this version has no set-product rows yet. */ -const showBuildActions = computed(() => !(props.products || []).some(p => p && p.productCode)) + +/** Resolve version id: prop first, else reload package current version. */ +const resolvedPsvId = ref(props.psvId) +watch(() => props.psvId, v => { if (v != null && v !== '') resolvedPsvId.value = v }) + +async function resolvePsvId(force) { + if (!force && resolvedPsvId.value != null && resolvedPsvId.value !== '') { + return resolvedPsvId.value + } + if (!props.priceSetId) return null + const res = await getPriceSet(props.priceSetId) + const data = res.data || {} + const versions = data.versions || [] + const fromData = (data.version && data.version.psvId) ? data.version : null + const current = fromData || versions[0] || {} + if (current.psvId != null) { + resolvedPsvId.value = current.psvId + emit('update:psvId', current.psvId) + } + return resolvedPsvId.value || null +} /** productCode -> detail rows (loaded on demand) */ const detailByProduct = reactive({}) @@ -218,8 +223,9 @@ function clearDetailCache() { async function loadProductDetails(productCode, force) { if (!productCode) return [] if (!force && detailByProduct[productCode]) return detailByProduct[productCode] - if (!props.psvId) return [] - const res = await listSetDetailByProduct(props.psvId, productCode) + const psvId = await resolvePsvId(false) + if (!psvId) return [] + const res = await listSetDetailByProduct(psvId, productCode) const rows = (res.data || []).map(d => hydrateRow(d, props.setType)) detailByProduct[productCode] = rows // keep parent details array in sync for any legacy callers @@ -258,7 +264,7 @@ const pulling = ref(false) function noop() {} -function openImport() { +async function openImport() { if (!props.priceSetId) { proxy.$modal.msgWarning('缺少套餐ID') return @@ -267,6 +273,11 @@ function openImport() { proxy.$modal.msgWarning('仅固定价套餐支持导入') return } + const psvId = await resolvePsvId(true) + if (!psvId) { + proxy.$modal.msgWarning('缺少套餐版本,请先保存基本信息') + return + } importRef.value && importRef.value.open() } @@ -284,7 +295,7 @@ async function runImportAsync(uploadFile) { try { const { task } = await importSetDetailPricesAsync( raw, - { priceSetId: props.priceSetId, psvId: props.psvId }, + { priceSetId: props.priceSetId, psvId: await resolvePsvId(false) }, applyBuildProgress ) importProgressPercent.value = 100 @@ -336,9 +347,14 @@ async function reloadDetailsFromServer() { const res = await getPriceSet(props.priceSetId) const data = res.data || {} const versions = data.versions || [] - const qPsv = props.psvId + const qPsv = resolvedPsvId.value || props.psvId const byQuery = qPsv != null ? versions.find(v => String(v.psvId) === String(qPsv)) : null - const current = byQuery || data.version || versions[0] || {} + const fromData = (data.version && data.version.psvId) ? data.version : null + const current = byQuery || fromData || versions[0] || {} + if (current.psvId != null) { + resolvedPsvId.value = current.psvId + emit('update:psvId', current.psvId) + } if (Array.isArray(props.details)) { props.details.splice(0, props.details.length) } @@ -387,7 +403,7 @@ async function pullAllFromLibrary(opts = {}) { pulling.value = true try { const { task } = await pullSetDetailsFromLibraryAsync( - { priceSetId: props.priceSetId, psvId: props.psvId, force }, + { priceSetId: props.priceSetId, psvId: await resolvePsvId(false), force }, applyBuildProgress ) importProgressPercent.value = 100 @@ -691,57 +707,11 @@ defineExpose({ pullAllFromLibrary }) font-size: 13px; word-break: break-all; } -.build-choice { - padding: 48px 24px 56px; - text-align: center; -} -.build-choice__tip { - margin: 0 0 28px; - color: #909399; - font-size: 14px; -} -.build-choice__cards { +.card-actions { + flex: none; display: flex; - justify-content: center; - gap: 20px; - flex-wrap: wrap; -} -.build-card { - width: 280px; - min-height: 120px; - padding: 24px 20px; - border: 1px solid #e4e7ed; - border-radius: 10px; - background: #fff; - cursor: pointer; - text-align: left; - transition: border-color 0.2s, box-shadow 0.2s; -} -.build-card:hover:not(:disabled) { - box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06); -} -.build-card:disabled { - opacity: 0.55; - cursor: not-allowed; -} -.build-card--pull:hover:not(:disabled) { - border-color: #67c23a; -} -.build-card--import:hover:not(:disabled) { - border-color: #409eff; -} -.build-card__title { - font-size: 16px; - font-weight: 600; - color: #303133; - margin-bottom: 10px; -} -.build-card--pull .build-card__title { color: #67c23a; } -.build-card--import .build-card__title { color: #409eff; } -.build-card__desc { - font-size: 13px; - line-height: 1.6; - color: #909399; + align-items: center; + gap: 8px; } .filters { display: flex; diff --git a/bms-front/src/views/price/set/create.vue b/bms-front/src/views/price/set/create.vue index 4a804e05..e9a2d8b4 100644 --- a/bms-front/src/views/price/set/create.vue +++ b/bms-front/src/views/price/set/create.vue @@ -16,6 +16,7 @@ :products="form.products" :price-set-id="form.priceSetId" :psv-id="form.version.psvId" + @update:psv-id="onPsvIdUpdate" :set-type="form.basic.setType" :version-no="form.version.versionNo" :ref-data="refData" @@ -141,7 +142,7 @@ async function runSaveWithProgress(payload) { } } -/** Step1 -> Step2: create empty draft version; pull/import on Step 2 */ +/** Step1 -> Step2: create empty draft version only; products/details persist on pull or Excel import */ async function goStep2() { const validBasic = await basicRef.value.validate() if (!validBasic) return @@ -153,10 +154,31 @@ async function goStep2() { creating.value = true try { - const priceSetId = await runSaveWithProgress(buildPayload()) + // Never auto-pull product library on enter Step2 (even if old clients sent the flag). + const payload = buildPayload() + delete payload.pullFromLibrary + delete payload.forcePullLibrary + delete payload.details + const priceSetId = await runSaveWithProgress(payload) form.priceSetId = priceSetId const fresh = await getPriceSet(priceSetId) applySetData(fresh.data || {}, false) + form.details = [] + form.products = [] + if (!form.version.psvId) { + const versions = (fresh.data && fresh.data.versions) || [] + const v = (fresh.data && fresh.data.version && fresh.data.version.psvId) + ? fresh.data.version + : versions[0] + if (v && v.psvId) { + form.version.psvId = v.psvId + form.version.versionNo = v.versionNo || form.version.versionNo + } + } + if (!form.version.psvId) { + proxy.$modal.msgError('套餐版本创建失败,请重试') + return + } active.value = 1 proxy.$modal.msgSuccess('已创建空套餐版本,请在第 2 步拉取或导入价格') } catch (e) { @@ -241,6 +263,10 @@ function goBack() { } } +function onPsvIdUpdate(psvId) { + if (psvId != null && psvId !== '') form.version.psvId = psvId +} + function applySetData(data, asCopy) { form.priceSetId = data.priceSetId Object.assign(form.basic, { @@ -252,7 +278,8 @@ function applySetData(data, asCopy) { const versions = data.versions || [] const qPsv = route.query.psvId const byQuery = qPsv != null ? versions.find(v => String(v.psvId) === String(qPsv)) : null - const current = byQuery || data.version || versions[0] || {} + const fromData = (data.version && data.version.psvId) ? data.version : null + const current = byQuery || fromData || versions[0] || {} if (asCopy) { form.version.psvId = undefined form.version.versionNo = nextVersionNo(versions)