修改产品功能

This commit is contained in:
wuweihua
2026-09-28 16:26:55 +08:00
parent 32ab61b51c
commit 623df69d4b
12 changed files with 217 additions and 93 deletions
@@ -39,4 +39,6 @@ public class BmsProduct extends BaseEntity
private List<BmsProductCountry> countries;
/** query alias of status (0/1/2) */
private String effectStatus;
/** query: active = status in (0,1); expired = status 2 */
private String statusGroup;
}
@@ -24,11 +24,6 @@ public class PrcPriceSet extends BaseEntity
private BigDecimal defaultDiscount;
private String defaultType;
private String calculateSign;
/**
* Pull-only: product base price version_no (V1/V2 from bms_product_price_version).
* Not a DB column — used when pullFromLibrary=Y to choose library rows.
*/
private String baseVersionNo;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date effectiveDate;
@JsonFormat(pattern = "yyyy-MM-dd")
@@ -1830,21 +1830,22 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
detailMapper.deletePrcPriceSetDetailByPsvId(version.getPsvId());
// clean legacy rows without psv_id under this set (would look like duplicates in UI)
detailMapper.deleteOrphanDetailsByPriceSetId(set.getPriceSetId());
String baseVersionNo = set.getBaseVersionNo();
if (StringUtils.isEmpty(baseVersionNo))
{
throw new ServiceException(MessageUtils.message("price.set.baseVersionNo.empty"));
}
int inserted;
if (TYPE_DISCOUNT.equals(set.getSetType()))
{
inserted = detailMapper.insertDiscountDetailsFromLibrary(
set.getPriceSetId(), version.getPsvId(), baseVersionNo);
set.getPriceSetId(), version.getPsvId(), null);
}
else
{
// Prefer currently effective open country rows; fallback asOf window if needed
inserted = detailMapper.insertFixedDetailsFromLibrary(
set.getPriceSetId(), version.getPsvId(), null, false, baseVersionNo);
set.getPriceSetId(), version.getPsvId(), null, true, null);
if (inserted <= 0)
{
inserted = detailMapper.insertFixedDetailsFromLibrary(
set.getPriceSetId(), version.getPsvId(), new Date(), false, null);
}
}
if (inserted <= 0)
{
@@ -96,6 +96,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="effectStatus != null and effectStatus != ''">
AND p.status = #{effectStatus}
</if>
<if test="statusGroup != null and statusGroup == 'active'">
AND p.status in ('0', '1')
</if>
<if test="statusGroup != null and statusGroup == 'expired'">
AND p.status = '2'
<if test="params != null and params.beginTime != null and params.beginTime != ''">
AND p.expire_date &gt;= #{params.beginTime}
</if>
<if test="params != null and params.endTime != null and params.endTime != ''">
AND p.expire_date &lt;= #{params.endTime}
</if>
</if>
<if test="cargoAttr != null and cargoAttr != ''">
AND p.cargo_attr like concat('%', #{cargoAttr}, '%')
</if>
@@ -104,7 +116,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
OR p.product_name like concat('%', #{params.keyword}, '%'))
</if>
</where>
order by p.product_code asc, p.effective_date desc, p.product_id desc
<choose>
<when test="statusGroup != null and statusGroup == 'expired'">
order by p.expire_date desc, p.product_code asc, p.product_id desc
</when>
<otherwise>
order by p.product_code asc, p.effective_date desc, p.product_id desc
</otherwise>
</choose>
</select>
<insert id="insertBmsProduct" parameterType="BmsProduct" useGeneratedKeys="true" keyProperty="productId">
@@ -171,6 +171,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and ifnull(c.status, '1') &lt;&gt; '2'
<choose>
<when test="openOnly">
and (c.effective_date is null or date(c.effective_date) &lt;= curdate())
and (c.expire_date is null or date(c.expire_date) &gt; curdate())
</when>
<otherwise>
-8
View File
@@ -63,11 +63,3 @@ export function delProduct(productIds) {
method: 'delete'
})
}
/** List base-price version options (by effective_date desc) for package create */
export function listProductPriceVersions() {
return request({
url: '/price/product/priceVersions',
method: 'get'
})
}
@@ -95,7 +95,6 @@ import { formatWeightLabel } from '@/views/price/set/utils/priceSet'
const { proxy } = getCurrentInstance()
const columns = [
{ prop: 'versionNo', label: '版本', width: 70, align: 'center', formatter: (row) => row.versionNo || '-' },
{ prop: 'countryName', label: '国家', width: 120, showOverflowTooltip: true },
// { prop: 'countryCode', label: 'ISO2', width: 60, align: 'center' },
{ prop: 'zoneName', label: '分区名称', width: 140, showOverflowTooltip: true, formatter: (row) => row.zoneName || '-' },
@@ -0,0 +1,137 @@
<template>
<el-drawer v-model="visible" size="960px" :title="title" append-to-body>
<el-form :inline="true" @submit.prevent>
<el-form-item label="产品代码">
<el-input
v-model="queryParams.productCode"
placeholder="产品代码"
clearable
style="width: 160px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="产品名称">
<el-input
v-model="queryParams.productName"
placeholder="产品名称"
clearable
style="width: 160px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="失效时间段">
<el-date-picker
v-model="dateRange"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 260px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="list" border max-height="calc(100vh - 260px)" empty-text="暂无失效产品">
<el-table-column label="产品代码" prop="productCode" min-width="110" show-overflow-tooltip />
<el-table-column label="产品名称" prop="productName" min-width="140" show-overflow-tooltip />
<el-table-column label="时效等级" prop="serviceLevel" width="90" align="center" />
<el-table-column label="走货属性" prop="cargoAttr" min-width="110" show-overflow-tooltip />
<el-table-column label="目的区域" prop="destRegion" width="90" align="center" />
<el-table-column label="生效日期" prop="effectiveDate" width="120" align="center" />
<el-table-column label="失效日期" prop="expireDate" width="120" align="center" />
<el-table-column label="国家数" prop="countryCount" width="80" align="center" />
<el-table-column label="附加费数" prop="surchargeCount" width="90" align="center" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<dict-tag :options="bms_product_effect_status" :value="row.status" />
</template>
</el-table-column>
<el-table-column label="更新内容" prop="updateNote" min-width="140" show-overflow-tooltip />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ row }">
<el-button
link
type="primary"
title="产品明细"
icon="View"
@click="emit('view-country', row)"
v-hasPermi="['price:product:query']"
></el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="loadList"
/>
</el-drawer>
</template>
<script setup>
import { listProduct } from '@/api/price/product'
const emit = defineEmits(['view-country'])
const { proxy } = getCurrentInstance()
const { bms_product_effect_status } = useDict('bms_product_effect_status')
const title = '失效价格'
const visible = ref(false)
const loading = ref(false)
const list = ref([])
const total = ref(0)
const dateRange = ref([])
const queryParams = ref({
pageNum: 1,
pageSize: 10,
productCode: undefined,
productName: undefined,
statusGroup: 'expired'
})
function open() {
visible.value = true
resetQuery()
}
function loadList() {
loading.value = true
const p = { ...queryParams.value }
if (dateRange.value && dateRange.value.length === 2) {
proxy.addDateRange(p, dateRange.value)
}
listProduct(p).then(res => {
list.value = res.rows || []
total.value = res.total || 0
}).finally(() => {
loading.value = false
})
}
function handleQuery() {
queryParams.value.pageNum = 1
loadList()
}
function resetQuery() {
dateRange.value = []
queryParams.value = {
pageNum: 1,
pageSize: 10,
productCode: undefined,
productName: undefined,
statusGroup: 'expired'
}
loadList()
}
defineExpose({ open })
</script>
@@ -10,6 +10,7 @@
<basic-table
ref="tableRef"
:extra-params="extraParams"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
@@ -32,6 +33,9 @@
</template>
</el-dropdown>
</el-col>
<el-col :span="1.5">
<el-button plain icon="Clock" @click="expiredRef.open()" v-hasPermi="['price:product:list']">失效价格</el-button>
</el-col>
</template>
<template #col-cargoAttr="{ row }">
@@ -52,6 +56,7 @@
<product-form-dialog ref="formRef" @success="() => tableRef.refresh()" />
<product-country-drawer ref="countryRef" />
<product-surcharge-drawer ref="surchargeRef" @success="() => tableRef.refresh()" />
<product-expired-drawer ref="expiredRef" @view-country="(row) => countryRef.open(row)" />
<excel-import-dialog
ref="importRef"
title="产品 Excel 导入"
@@ -99,16 +104,18 @@ import ExcelImportDialog from '@/components/ExcelImportDialog'
import ProductFormDialog from './components/ProductFormDialog.vue'
import ProductCountryDrawer from './components/ProductCountryDrawer.vue'
import ProductSurchargeDrawer from './components/ProductSurchargeDrawer.vue'
import ProductExpiredDrawer from './components/ProductExpiredDrawer.vue'
import { listProduct } from '@/api/price/product'
import { listRegionOptions } from '@/api/data/region'
const { proxy } = getCurrentInstance()
const { bms_product_effect_status } = useDict('bms_product_effect_status')
const extraParams = { statusGroup: 'active' }
const tableRef = ref()
const formRef = ref()
const countryRef = ref()
const surchargeRef = ref()
const expiredRef = ref()
const importRef = ref()
const countryImportRef = ref()
const importCountryCode = ref('')
@@ -144,24 +151,12 @@ const columns = [
const searchColumns = [
{ prop: 'productCode', label: '产品代码', component: 'input', placeholder: '产品代码' },
{ prop: 'productName', label: '产品名称', component: 'input', placeholder: '产品名称' }
// {
// prop: 'taxMode',
// label: '税模式',
// component: 'select',
// dict: 'bms_tax_mode',
// placeholder: '税模式',
// clearable: true
// }
]
function queryList(params) {
return listProduct(params)
}
function isProductActive(row) {
return row && String(row.status) === '1'
}
function handleAdd() {
formRef.value.open()
}
@@ -7,25 +7,6 @@
<div class="form-hint">显示给客户的套餐对外名称</div>
</el-form-item>
<el-form-item label="基础价格版本" prop="baseVersionNo">
<el-select
v-model="basic.baseVersionNo"
filterable
clearable
placeholder="请选择基础价格版本"
style="width: 100%"
:loading="versionLoading"
>
<el-option
v-for="(item, idx) in versionOptions"
:key="item.versionNo"
:label="versionLabel(item, idx === 0)"
:value="item.versionNo"
/>
</el-select>
<div class="form-hint">创建套餐价格时按此版本从产品库拉取明细;默认最新版本</div>
</el-form-item>
<el-form-item label="计价类型" prop="setType">
<div class="type-row">
<div
@@ -69,41 +50,19 @@
<script setup name="StepBasicInfo">
/**
* Step1 basic info. baseVersionNo is pull-only (not persisted).
* 第 1 步 · 基本信息
* 直接读写父级传入的 basic 对象(同一引用),校验规则由本组件维护。
*/
import { listProductPriceVersions } from '@/api/price/product'
const basic = defineModel({ type: Object, required: true })
const formRef = ref()
const versionLoading = ref(false)
const versionOptions = ref([])
const rules = {
priceSetName: [{ required: true, message: '套餐名称不能为空', trigger: 'blur' }],
baseVersionNo: [{ required: true, message: '请选择基础价格版本', trigger: 'change' }],
setType: [{ required: true, message: '请选择计价类型', trigger: 'change' }],
}
function versionLabel(item, isLatest) {
const no = item.versionNo || '-'
return no
}
async function loadVersions() {
versionLoading.value = true
try {
const res = await listProductPriceVersions()
const list = res.data || []
versionOptions.value = list
if (!basic.value.baseVersionNo && list.length) {
basic.value.baseVersionNo = list[0].versionNo
}
} finally {
versionLoading.value = false
}
}
/** 供父级在下一步 / 保存前调用 */
function validate() {
return new Promise(resolve => {
if (!formRef.value) return resolve(false)
@@ -111,20 +70,46 @@ function validate() {
})
}
onMounted(() => {
loadVersions()
})
defineExpose({ validate })
</script>
<style scoped>
.max-form { max-width: 760px; }
.form-hint { font-size: 12px; color: #909399; line-height: 1.4; margin-top: 4px; }
.unit { margin-left: 8px; color: #909399; }
.type-row { display: flex; gap: 12px; width: 100%; }
.type-card { flex: 1; border: 1px solid #d9d9d9; border-radius: 6px; padding: 14px 16px; cursor: pointer; }
.type-card.selected { border-color: #1890ff; background: #e6f7ff; }
.t-title { font-weight: 600; margin-bottom: 4px; }
.t-desc { font-size: 12px; color: #909399; line-height: 1.5; }
.max-form {
max-width: 760px;
}
.form-hint {
font-size: 12px;
color: #909399;
line-height: 1.4;
margin-top: 4px;
}
.unit {
margin-left: 8px;
color: #909399;
}
.type-row {
display: flex;
gap: 12px;
width: 100%;
}
.type-card {
flex: 1;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 14px 16px;
cursor: pointer;
}
.type-card.selected {
border-color: #1890ff;
background: #e6f7ff;
}
.t-title {
font-weight: 600;
margin-bottom: 4px;
}
.t-desc {
font-size: 12px;
color: #909399;
line-height: 1.5;
}
</style>
-1
View File
@@ -220,7 +220,6 @@ function buildPayload() {
setType: basic.setType,
defaultDiscount: basic.setType === 'DISCOUNT' ? basic.defaultDiscount : undefined,
priceSetNote: basic.priceSetNote,
baseVersionNo: basic.baseVersionNo || undefined,
version: {
psvId: copyMode.value ? undefined : version.psvId,
versionNo: version.versionNo
@@ -361,7 +361,6 @@ export function createEmptyBasicInfo() {
defaultDiscount: 88,
effectiveDate: undefined,
expireDate: undefined,
baseVersionNo: undefined,
priceSetNote: ''
}
}