增加删除功能

This commit is contained in:
wuweihua
2026-10-10 13:52:28 +08:00
parent fab2ec7d9c
commit eeaea52c17
8 changed files with 144 additions and 4 deletions
@@ -112,6 +112,7 @@ price.zone.countryCode.empty=国家编码不能为空,请检查 region.country
# 产品导入 BmsProduct # 产品导入 BmsProduct
price.product.codeDate.exists=同一产品代码与生效日期已存在,不能覆盖历史数据 price.product.codeDate.exists=同一产品代码与生效日期已存在,不能覆盖历史数据
price.product.effectiveDate.empty=生效日期不能为空 price.product.effectiveDate.empty=生效日期不能为空
price.product.delete.pendingOnly=仅待生效产品可删除,以下不符合:{0}
price.product.import.effEmpty=缺少生效日期,已跳过 price.product.import.effEmpty=缺少生效日期,已跳过
price.product.import.effRequired=请选择生效时间 price.product.import.effRequired=请选择生效时间
price.product.import.fileEmpty=上传文件不能为空 price.product.import.fileEmpty=上传文件不能为空
@@ -39,6 +39,11 @@ public interface BmsProductCountryMapper
@Param("productCode") String productCode, @Param("productCode") String productCode,
@Param("newEffectiveDate") Date newEffectiveDate); @Param("newEffectiveDate") Date newEffectiveDate);
/** After deleting a pending version: reopen country rows closed at that effective date */
public int reopenExpiredByProductCodeAndExpireDate(
@Param("productCode") String productCode,
@Param("expireDate") Date expireDate);
/** /**
* Close open rows for one product + country where effective_date < newEffectiveDate. * Close open rows for one product + country where effective_date < newEffectiveDate.
* Match by country_code when present, otherwise country_name. * Match by country_code when present, otherwise country_name.
@@ -39,6 +39,11 @@ public interface BmsProductMapper
@Param("newEffectiveDate") Date newEffectiveDate, @Param("newEffectiveDate") Date newEffectiveDate,
@Param("excludeId") Long excludeId); @Param("excludeId") Long excludeId);
/** After deleting a pending version: reopen rows closed at that effective date */
public int reopenExpiredByProductCodeAndExpireDate(
@Param("productCode") String productCode,
@Param("expireDate") Date expireDate);
/** Recompute status from effective_date / expire_date vs today */ /** Recompute status from effective_date / expire_date vs today */
public int refreshStatusByDates(); public int refreshStatusByDates();
} }
@@ -174,6 +174,9 @@ public class BmsProductServiceImpl implements IBmsProductService
return 0; return 0;
} }
List<String> codesNoRemain = new ArrayList<>(); List<String> codesNoRemain = new ArrayList<>();
List<Long> deletableIds = new ArrayList<>();
List<String> notPendingTips = new ArrayList<>();
List<BmsProduct> toReopen = new ArrayList<>();
for (Long id : productIds) for (Long id : productIds)
{ {
BmsProduct p = productMapper.selectBmsProductById(id); BmsProduct p = productMapper.selectBmsProductById(id);
@@ -181,14 +184,46 @@ public class BmsProductServiceImpl implements IBmsProductService
{ {
continue; continue;
} }
String liveStatus = resolveStoredStatus(p.getEffectiveDate(), p.getExpireDate());
if (!BmsProduct.STATUS_PENDING.equals(liveStatus))
{
String tip = p.getProductCode();
if (p.getEffectiveDate() != null)
{
tip = tip + "(" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(p.getEffectiveDate()) + ")";
}
notPendingTips.add(tip);
continue;
}
if (p.getProductId() != null) if (p.getProductId() != null)
{ {
countryMapper.deleteByProductId(p.getProductId()); countryMapper.deleteByProductId(p.getProductId());
} }
countryMapper.deleteByProductCodeAndEffectiveDate(p.getProductCode(), p.getEffectiveDate()); countryMapper.deleteByProductCodeAndEffectiveDate(p.getProductCode(), p.getEffectiveDate());
codesNoRemain.add(p.getProductCode()); codesNoRemain.add(p.getProductCode());
deletableIds.add(p.getProductId());
if (p.getEffectiveDate() != null)
{
toReopen.add(p);
}
} }
int rows = productMapper.deleteBmsProductByIds(productIds); if (!notPendingTips.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.delete.pendingOnly",
String.join("\u3001", notPendingTips)));
}
if (deletableIds.isEmpty())
{
return 0;
}
int rows = productMapper.deleteBmsProductByIds(deletableIds.toArray(new Long[0]));
for (BmsProduct p : toReopen)
{
productMapper.reopenExpiredByProductCodeAndExpireDate(p.getProductCode(), p.getEffectiveDate());
countryMapper.reopenExpiredByProductCodeAndExpireDate(p.getProductCode(), p.getEffectiveDate());
}
productMapper.refreshStatusByDates();
countryMapper.refreshStatusByDates();
List<String> orphanCodes = new ArrayList<>(); List<String> orphanCodes = new ArrayList<>();
for (String code : codesNoRemain) for (String code : codesNoRemain)
{ {
@@ -171,6 +171,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and (effective_date is null or effective_date &lt; #{newEffectiveDate}) and (effective_date is null or effective_date &lt; #{newEffectiveDate})
</update> </update>
<update id="reopenExpiredByProductCodeAndExpireDate">
update product_country
set expire_date = null,
update_time = sysdate()
where product_code = #{productCode}
and expire_date = #{expireDate}
</update>
<update id="expireOpenByProductCodeAndCountry"> <update id="expireOpenByProductCodeAndCountry">
update product_country update product_country
set expire_date = #{newEffectiveDate}, set expire_date = #{newEffectiveDate},
@@ -207,6 +207,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</if> </if>
</update> </update>
<update id="reopenExpiredByProductCodeAndExpireDate">
update product
set expire_date = null,
update_time = sysdate()
where product_code = #{productCode}
and expire_date = #{expireDate}
</update>
<update id="refreshStatusByDates"> <update id="refreshStatusByDates">
update product update product
set status = case set status = case
@@ -0,0 +1,11 @@
-- Add product batch-delete button permission (idempotent)
SET @menuId := (
SELECT menu_id FROM sys_menu
WHERE perms = 'price:product:list' OR component LIKE '%price/basic/product%'
ORDER BY menu_id ASC LIMIT 1
);
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT '产品删除', @menuId, 6, '', '', 1, 0, 'F', '0', '0', 'price:product:remove', '#', 'admin', sysdate()
WHERE @menuId IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'price:product:remove');
@@ -15,6 +15,7 @@
:search-columns="searchColumns" :search-columns="searchColumns"
:query="queryList" :query="queryList"
row-key="productId" row-key="productId"
:selection="true"
> >
<template #toolbar="{ selection }"> <template #toolbar="{ selection }">
<el-col :span="1.5"> <el-col :span="1.5">
@@ -33,6 +34,16 @@
</template> </template>
</el-dropdown> </el-dropdown>
</el-col> </el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="!selection.length"
@click="handleBatchDelete(selection)"
v-hasPermi="['price:product:remove']"
>批量删除</el-button>
</el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button plain icon="Clock" @click="expiredRef.open()" v-hasPermi="['price:product:list']">失效价格</el-button> <el-button plain icon="Clock" @click="expiredRef.open()" v-hasPermi="['price:product:list']">失效价格</el-button>
</el-col> </el-col>
@@ -152,9 +163,10 @@ import ProductExcelFileDrawer from './components/ProductExcelFileDrawer.vue'
import ProductCountryDrawer from './components/ProductCountryDrawer.vue' import ProductCountryDrawer from './components/ProductCountryDrawer.vue'
import ProductSurchargeDrawer from './components/ProductSurchargeDrawer.vue' import ProductSurchargeDrawer from './components/ProductSurchargeDrawer.vue'
import ProductExpiredDrawer from './components/ProductExpiredDrawer.vue' import ProductExpiredDrawer from './components/ProductExpiredDrawer.vue'
import { listProduct, importProductExcelAsync, importProductCountryExcelAsync } from '@/api/price/product' import { listProduct, delProduct, importProductExcelAsync, importProductCountryExcelAsync } from '@/api/price/product'
import { listRegionOptions } from '@/api/data/region' import { listRegionOptions } from '@/api/data/region'
const { proxy } = getCurrentInstance()
const { bms_product_effect_status } = useDict('bms_product_effect_status') const { bms_product_effect_status } = useDict('bms_product_effect_status')
const extraParams = { statusGroup: 'active' } const extraParams = { statusGroup: 'active' }
@@ -225,11 +237,24 @@ const columns = [
const searchColumns = [ const searchColumns = [
{ prop: 'productCode', label: '产品代码', component: 'input', placeholder: '产品代码' }, { prop: 'productCode', label: '产品代码', component: 'input', placeholder: '产品代码' },
{ prop: 'productName', label: '产品名称', component: 'input', placeholder: '产品名称' } { prop: 'productName', label: '产品名称', component: 'input', placeholder: '产品名称' },
{
prop: 'status',
label: '状态',
component: 'select',
dict: 'bms_product_effect_status',
placeholder: '请选择状态',
width: '160px'
}
] ]
function queryList(params) { function queryList(params) {
return listProduct(params) const p = { ...(params || {}) }
// Exact status search must not be limited by default statusGroup=active.
if (p.status != null && p.status !== '') {
delete p.statusGroup
}
return listProduct(p)
} }
function handleAdd() { function handleAdd() {
@@ -240,6 +265,48 @@ function handleUpdate(row) {
formRef.value.open(row) formRef.value.open(row)
} }
function isPendingProduct(row) {
if (!row) return false
if (String(row.status) === '0') return true
// fallback: effective in future and not expired
if (!row.effectiveDate) return false
const eff = new Date(String(row.effectiveDate).replace(/-/g, '/')).getTime()
if (Number.isNaN(eff) || eff <= Date.now()) return false
if (row.expireDate) {
const exp = new Date(String(row.expireDate).replace(/-/g, '/')).getTime()
if (!Number.isNaN(exp) && exp <= Date.now()) return false
}
return true
}
function handleBatchDelete(selection) {
const rows = (selection || []).filter(r => r && r.productId != null)
if (!rows.length) {
proxy.$modal.msgWarning('请先勾选产品')
return
}
const pending = rows.filter(isPendingProduct)
const blocked = rows.filter(r => !isPendingProduct(r))
if (!pending.length) {
proxy.$modal.msgWarning('仅待生效产品可删除,当前勾选无待生效记录')
return
}
if (blocked.length) {
const tip = blocked.map(r => r.productCode || r.productId).join('、')
proxy.$modal.msgWarning('已跳过非待生效产品:' + tip)
}
const codes = pending.map(r => r.productCode || r.productId).join('、')
const ids = pending.map(r => r.productId)
proxy.$modal.confirm(
'确认删除已选待生效产品(' + pending.length + '个):'
+ codes
+ '?删除后产品及国家/重量段明细将一并清除,且不可恢复。'
).then(() => delProduct(ids)).then(() => {
tableRef.value.refresh()
proxy.$modal.msgSuccess('删除成功')
}).catch(() => {})
}
function handleImportCommand(command) { function handleImportCommand(command) {
if (command === 'country') { if (command === 'country') {
handleCountryImport() handleCountryImport()