优化套餐新建版本的逻辑

This commit is contained in:
wuweihua
2026-10-28 22:09:30 +08:00
parent 786feec859
commit 56340d0ea0
7 changed files with 177 additions and 47 deletions
@@ -205,6 +205,7 @@ price.bill.fee.idRequired=费用ID不能为空
price.set.version.effectiveTime.empty=版本生效时间不能为空
price.set.version.effectiveTime.beforePrev=生效时间不能早于上一版本的生效时间
price.set.version.effectiveTime.hitBoundary=生效时间不能与已有版本的生效时间或失效时间相同
price.set.version.notExists=套餐版本不存在
price.set.version.activate.statusNotAllowed=仅草稿或待生效版本可设置生效时间
price.set.version.delete.statusNotAllowed=仅草稿、待生效或已生效版本可删除
@@ -401,10 +401,20 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
throw new ServiceException(MessageUtils.message("price.set.edit.stopped"));
}
Date prevEffective = findPrevVersionEffectiveTime(version);
if (prevEffective != null && effectiveTime.before(prevEffective))
boolean importFresh = "IMPORT_FRESH".equalsIgnoreCase(StringUtils.trim(version.getPriceBuildMode()));
if (importFresh)
{
throw new ServiceException(MessageUtils.message("price.set.version.effectiveTime.beforePrev"));
// May start inside a prior version window; forbid equal to any other version's eff/exp.
// refreshVersionStatus then sets B.expire=A.eff, A.expire=B.oldExpire and syncs children.
validateImportFreshActivateTime(version, effectiveTime);
}
else
{
Date prevEffective = findPrevVersionEffectiveTime(version);
if (prevEffective != null && effectiveTime.before(prevEffective))
{
throw new ServiceException(MessageUtils.message("price.set.version.effectiveTime.beforePrev"));
}
}
// Set schedule; refreshVersionStatus decides PENDING(3) vs EFFECT(1) and chains expire_time
version.setEffectiveTime(effectiveTime);
@@ -415,6 +425,41 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
return 1;
}
/**
* IMPORT_FRESH activate: allow effective time earlier than prev-by-seq (incl. inside prior window B).
* Must not equal any other timed version's effectiveTime or expireTime.
*/
private void validateImportFreshActivateTime(PrcPriceSetVersion version, Date effectiveTime)
{
if (version == null || version.getPriceSetId() == null || effectiveTime == null)
{
return;
}
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceSetId(version.getPriceSetId());
q.setPriceType(com.bms.price.domain.PriceType.SALES);
List<PrcPriceSetVersion> list = versionMapper.selectPrcPriceSetVersionList(q);
if (list == null || list.isEmpty())
{
return;
}
for (PrcPriceSetVersion v : list)
{
if (v == null)
{
continue;
}
if (version.getPsvId() != null && version.getPsvId().equals(v.getPsvId()))
{
continue;
}
if (sameTime(v.getEffectiveTime(), effectiveTime) || sameTime(v.getExpireTime(), effectiveTime))
{
throw new ServiceException(MessageUtils.message("price.set.version.effectiveTime.hitBoundary"));
}
}
}
/** Previous version (lower version_seq) effective_time; null if none. */
private Date findPrevVersionEffectiveTime(PrcPriceSetVersion version)
{
@@ -58,6 +58,7 @@
<div class="build-card__desc">按产品库基础价生成套餐产品列表,可再导入 Excel 修改价格</div>
</button>
<button
v-if="showImportInherit"
type="button"
class="build-card build-card--import"
:class="{ selected: isImportBuildMode(priceBuildMode) }"
@@ -247,7 +248,12 @@ const props = defineProps({
/** True when build mode changed locally and not saved yet */
buildModeDirty: { type: Boolean, default: false },
/** False while parent still loading package data — avoid first click being wiped by applySetData */
buildModeReady: { type: Boolean, default: true }
buildModeReady: { type: Boolean, default: true },
/**
* Show「按上一版 Excel 建价」(IMPORT). Hidden for brand-new package create
* (no prior version); keep for newVersion / copy / edit.
*/
showImportInherit: { type: Boolean, default: true }
})
const emit = defineEmits(['update:psvId', 'update:priceBuildMode', 'update:pefId'])
@@ -173,7 +173,8 @@ const noneSc = '无'
const isFixed = computed(() => isFixedType(props.setType))
const title = computed(() => {
const v = props.version || {}
return (v.versionNo || '-') + ' · 版本明细'
const eff = v.effectiveTime || '未设置生效时间'
return eff + ' · 版本明细'
})
const detailTitleText = computed(() => {
const p = activeProduct.value || {}
@@ -19,13 +19,10 @@
</el-tabs>
<el-table :data="filteredVersions" border empty-text="暂无版本">
<el-table-column :label="colNo" width="100">
<template #default="{ row }">
<el-button link type="primary" @click="openDetails(row)">{{ row.versionNo || '-' }}</el-button>
</template>
</el-table-column>
<el-table-column :label="colEff" min-width="170">
<template #default="{ row }">{{ row.effectiveTime || '-' }}</template>
<template #default="{ row }">
<el-button link type="primary" @click="openDetails(row)">{{ row.effectiveTime || '未设置' }}</el-button>
</template>
</el-table-column>
<el-table-column :label="colExp" min-width="170">
<template #default="{ row }">{{ row.expireTime || '-' }}</template>
@@ -90,7 +87,6 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue', 'create-version', 'open-details', 'view-import-attach', 'activate', 'reschedule', 'edit', 'remove'])
const title = '套餐版本'
const colNo = '版本号'
const colEff = '生效时间'
const colExp = '失效时间'
const colStatus = '状态'
@@ -121,10 +117,33 @@ function tabLabel(key) {
const hasDraft = computed(() => (props.versions || []).some(v => isDraftVersion(v)))
function toEffMs(v) {
if (!v || !v.effectiveTime) return NaN
const t = new Date(String(v.effectiveTime).replace(/-/g, '/')).getTime()
return Number.isNaN(t) ? NaN : t
}
/** Sort by effective time descending; drafts without time last. */
function sortByEffectiveTime(list) {
return (list || []).slice().sort((a, b) => {
const ta = toEffMs(a)
const tb = toEffMs(b)
const aNull = Number.isNaN(ta)
const bNull = Number.isNaN(tb)
if (aNull && bNull) return (Number(b.versionSeq) || 0) - (Number(a.versionSeq) || 0)
if (aNull) return 1
if (bNull) return -1
if (tb !== ta) return tb - ta
return (Number(b.versionSeq) || 0) - (Number(a.versionSeq) || 0)
})
}
const filteredVersions = computed(() => {
const list = props.versions || []
if (statusTab.value === 'all') return list
return list.filter(v => String(v.status) === statusTab.value)
const filtered = statusTab.value === 'all'
? list
: list.filter(v => String(v.status) === statusTab.value)
return sortByEffectiveTime(filtered)
})
function detailCount(row) {
+5 -2
View File
@@ -22,6 +22,7 @@
:live-persist="livePersist"
:build-mode-dirty="buildModeDirty"
:build-mode-ready="pageReady"
:show-import-inherit="showImportInherit"
:header-title="detailsHeaderTitle"
/>
</div>
@@ -82,6 +83,8 @@ const deferPersist = computed(() => copyMode.value || newVersionMode.value)
const showBasic = computed(() => !deferPersist.value && String(route.query.step || '') !== '2')
/** Edit existing version: live PUT/pull/import. New create / copy / newVersion: defer until finish. */
const livePersist = computed(() => !!form.priceSetId && !deferPersist.value)
/** Brand-new package has no prior version — hide IMPORT inherit card. */
const showImportInherit = computed(() => newVersionMode.value || copyMode.value)
const detailsHeaderTitle = '编辑产品价格'
@@ -382,7 +385,7 @@ function onPriceBuildModeUpdate(mode) {
proxy.$modal.msgSuccess("已拉取产品列表,可继续编辑价格")
}).catch(() => {})
})
} else if (mode === 'IMPORT') {
} else if (mode === 'IMPORT' && showImportInherit.value) {
nextTick(async () => {
// newVersion/copy: wait until loadExisting fills priceSetId
const qid = route.query.priceSetId
@@ -397,7 +400,7 @@ function onPriceBuildModeUpdate(mode) {
return
}
if (!form.priceSetId) {
proxy.$modal.msgSuccess("新建套餐无历史版本可带出,请直接导入 Excel 建价")
proxy.$modal.msgSuccess("请直接导入 Excel 建价")
return
}
try {
+86 -31
View File
@@ -50,9 +50,6 @@
@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"
@@ -92,7 +89,7 @@ import { usePriceSetRefData } from './composables/usePriceSetRefData'
import {
getPriceSet, disablePriceSet, enablePriceSet, activatePriceSetVersion, deletePriceSetVersion
} from '@/api/price/set'
import { isDraftVersion, isPendingVersion, isEffectVersion } from './utils/priceSet'
import { isDraftVersion, isPendingVersion, isEffectVersion, isImportFreshBuildMode } from './utils/priceSet'
const { proxy } = getCurrentInstance()
const route = useRoute()
@@ -233,8 +230,18 @@ const activateConfirmText = computed(() =>
activateMode.value === 'reschedule' ? '确认修改' : '确认启动'
)
const activatePrevEffectiveTime = ref(undefined)
const activateIsImportFresh = ref(false)
/** Other versions' effective/expire times (hour) — forbidden for IMPORT_FRESH */
const activateBoundaryTimes = ref([])
const activateEffBeforePrevMsg = '生效时间不能早于上一版本的生效时间'
const activateEffHitBoundaryMsg = '生效时间不能与已有版本的生效时间或失效时间相同'
const activateDialogHint = computed(() => {
if (activateIsImportFresh.value) {
const base = activateMode.value === 'reschedule'
? 'Excel 重新建价:生效时间可落在历史版本区间内,将自动截断该版本失效时间并同步产品/明细。'
: 'Excel 重新建价:生效时间可落在历史版本区间内;落在版本 B 区间时,B 失效时间改为本次启动时间,本版本失效时间取 B 原失效时间,并同步双方产品/明细。'
return base + '不可与已有版本的生效时间或失效时间相同。'
}
const base = activateMode.value === 'reschedule'
? '仅待生效版本可修改;若时间晚于当前仍为待生效,到时自动生效并使上一版本失效。'
: '若生效时间晚于当前,版本将进入「待生效」;到时自动生效并使上一版本失效。'
@@ -242,20 +249,27 @@ const activateDialogHint = computed(() => {
if (!prev) return base
return base + '不得早于上一版:' + prev + '。'
})
const activateRules = {
const activateRules = computed(() => ({
effectiveTime: [
{ required: true, message: '请选择生效时间', trigger: 'change' },
{
validator: (_r, v, cb) => {
if (!v) return cb()
if (activateIsImportFresh.value) {
const ms = toTimeMs(v)
if (activateBoundaryTimes.value.some(b => toTimeMs(b) === ms)) {
return cb(new Error(activateEffHitBoundaryMsg))
}
return cb()
}
const prev = activatePrevEffectiveTime.value
if (!v || !prev) return cb()
if (toTimeMs(v) < toTimeMs(prev)) return cb(new Error(activateEffBeforePrevMsg))
if (prev && toTimeMs(v) < toTimeMs(prev)) return cb(new Error(activateEffBeforePrevMsg))
cb()
},
trigger: 'change'
}
]
}
}))
function toTimeMs(val) {
if (val == null || val === '') return NaN
@@ -278,13 +292,30 @@ function findPrevVersionEffectiveTime(row) {
return prev ? normalizeToHour(prev.effectiveTime) : undefined
}
function pickActivateDefaultTime(prev) {
/** Collect other versions' effective/expire hour times (forbidden for IMPORT_FRESH). */
function collectActivateBoundaryTimes(excludePsvId) {
const out = []
const seen = new Set()
for (const v of set.value.versions || []) {
if (!v || v.psvId == null || String(v.psvId) === String(excludePsvId)) continue
;[v.effectiveTime, v.expireTime].forEach(t => {
const n = t ? normalizeToHour(t) : ''
if (!n || seen.has(n)) return
seen.add(n)
out.push(n)
})
}
return out
}
function pickActivateDefaultTime(prev, importFresh) {
const now = formatNow()
if (!prev) return now
if (importFresh || !prev) return now
return toTimeMs(now) >= toTimeMs(prev) ? now : prev
}
function disabledActivateDate(date) {
if (activateIsImportFresh.value) return false
const prev = activatePrevEffectiveTime.value
if (!prev || !date) return false
const prevDay = new Date(String(prev).replace(/-/g, '/'))
@@ -295,13 +326,27 @@ function disabledActivateDate(date) {
}
function disabledActivateHours() {
const prev = activatePrevEffectiveTime.value
const cur = activateForm.effectiveTime
if (!prev) return []
const prevD = new Date(String(prev).replace(/-/g, '/'))
const curD = cur
? new Date(String(normalizeToHour(cur)).replace(/-/g, '/'))
: new Date()
if (activateIsImportFresh.value) {
const hours = new Set()
for (const b of activateBoundaryTimes.value) {
const bd = new Date(String(b).replace(/-/g, '/'))
if (
bd.getFullYear() === curD.getFullYear() &&
bd.getMonth() === curD.getMonth() &&
bd.getDate() === curD.getDate()
) {
hours.add(bd.getHours())
}
}
return [...hours]
}
const prev = activatePrevEffectiveTime.value
if (!prev) return []
const prevD = new Date(String(prev).replace(/-/g, '/'))
if (
prevD.getFullYear() !== curD.getFullYear() ||
prevD.getMonth() !== curD.getMonth() ||
@@ -321,30 +366,38 @@ function onActivatePickerVisible(visible) {
}
}
function onActivateVersion(row) {
function openActivateDialog(row, mode) {
if (!row || row.psvId == null) return
activateMode.value = 'activate'
activateMode.value = mode
activateForm.psvId = row.psvId
activateForm.versionNo = row.versionNo || ''
activateIsImportFresh.value = isImportFreshBuildMode(row.priceBuildMode)
activatePrevEffectiveTime.value = findPrevVersionEffectiveTime(row)
activateForm.effectiveTime = pickActivateDefaultTime(activatePrevEffectiveTime.value)
activateBoundaryTimes.value = activateIsImportFresh.value
? collectActivateBoundaryTimes(row.psvId)
: []
const prev = activatePrevEffectiveTime.value
if (mode === 'reschedule') {
const cur = normalizeToHour(row.effectiveTime)
if (activateIsImportFresh.value) {
activateForm.effectiveTime = cur || pickActivateDefaultTime(prev, true)
} else if (cur && (!prev || toTimeMs(cur) >= toTimeMs(prev))) {
activateForm.effectiveTime = cur
} else {
activateForm.effectiveTime = pickActivateDefaultTime(prev, false)
}
} else {
activateForm.effectiveTime = pickActivateDefaultTime(prev, activateIsImportFresh.value)
}
activateVisible.value = true
}
function onActivateVersion(row) {
openActivateDialog(row, 'activate')
}
function onRescheduleVersion(row) {
if (!row || row.psvId == null) return
activateMode.value = 'reschedule'
activateForm.psvId = row.psvId
activateForm.versionNo = row.versionNo || ''
activatePrevEffectiveTime.value = findPrevVersionEffectiveTime(row)
const cur = normalizeToHour(row.effectiveTime)
const prev = activatePrevEffectiveTime.value
if (cur && (!prev || toTimeMs(cur) >= toTimeMs(prev))) {
activateForm.effectiveTime = cur
} else {
activateForm.effectiveTime = pickActivateDefaultTime(prev)
}
activateVisible.value = true
openActivateDialog(row, 'reschedule')
}
function onActivateClosed() {
@@ -353,6 +406,8 @@ function onActivateClosed() {
activateForm.versionNo = ''
activateForm.effectiveTime = undefined
activatePrevEffectiveTime.value = undefined
activateIsImportFresh.value = false
activateBoundaryTimes.value = []
activateFormRef.value && activateFormRef.value.clearValidate()
}
@@ -394,8 +449,8 @@ function onCreateVersion() {
function onRemoveVersion(row) {
if (!row || row.psvId == null) return
const msg = isEffectVersion(row)
? "确认软删除已生效版本 " + (row.versionNo || '') + "?版本及产品、明细将标记为已删除,上一版将自动恢复生效并清空失效时间。"
: "确认删除" + (isPendingVersion(row) ? "待生效" : "草稿") + "版本 " + (row.versionNo || '') + "?其明细与附加费将一并删除。"
? "确认软删除已生效版本(生效时间:" + (row.effectiveTime || '未设置') + ")?版本及产品、明细将标记为已删除,上一版将自动恢复生效并清空失效时间。"
: "确认删除" + (isPendingVersion(row) ? "待生效" : "草稿") + "版本(生效时间:" + (row.effectiveTime || '未设置') + ")?其明细与附加费将一并删除。"
proxy.$modal.confirm(msg).then(() => {
removingPsvId.value = row.psvId
proxy.$modal.loading("正在删除,请稍候...")