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 ec80c530..60aca24a 100644 --- a/bms-back/bms-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/bms-admin/src/main/resources/i18n/messages.properties @@ -112,6 +112,7 @@ price.zone.countryCode.empty=国家编码不能为空,请检查 region.country # 产品导入 BmsProduct price.product.codeDate.exists=同一产品代码与生效日期已存在,不能覆盖历史数据 price.product.effectiveDate.empty=生效日期不能为空 +price.product.delete.pendingOnly=仅待生效产品可删除,以下不符合:{0} price.product.import.effEmpty=缺少生效日期,已跳过 price.product.import.effRequired=请选择生效时间 price.product.import.fileEmpty=上传文件不能为空 diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/BmsProductCountryMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/BmsProductCountryMapper.java index 5eb90276..499c126f 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/BmsProductCountryMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/BmsProductCountryMapper.java @@ -39,6 +39,11 @@ public interface BmsProductCountryMapper @Param("productCode") String productCode, @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. * Match by country_code when present, otherwise country_name. diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/BmsProductMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/BmsProductMapper.java index 72512f7e..5321b3d7 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/BmsProductMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/BmsProductMapper.java @@ -39,6 +39,11 @@ public interface BmsProductMapper @Param("newEffectiveDate") Date newEffectiveDate, @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 */ public int refreshStatusByDates(); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java index 0e4289e7..98707e1c 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java @@ -174,6 +174,9 @@ public class BmsProductServiceImpl implements IBmsProductService return 0; } List codesNoRemain = new ArrayList<>(); + List deletableIds = new ArrayList<>(); + List notPendingTips = new ArrayList<>(); + List toReopen = new ArrayList<>(); for (Long id : productIds) { BmsProduct p = productMapper.selectBmsProductById(id); @@ -181,14 +184,46 @@ public class BmsProductServiceImpl implements IBmsProductService { 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) { countryMapper.deleteByProductId(p.getProductId()); } countryMapper.deleteByProductCodeAndEffectiveDate(p.getProductCode(), p.getEffectiveDate()); 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 orphanCodes = new ArrayList<>(); for (String code : codesNoRemain) { diff --git a/bms-back/bms-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml index 155c143e..5d1b4ed5 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml @@ -171,6 +171,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and (effective_date is null or effective_date < #{newEffectiveDate}) + + update product_country + set expire_date = null, + update_time = sysdate() + where product_code = #{productCode} + and expire_date = #{expireDate} + + update product_country set expire_date = #{newEffectiveDate}, diff --git a/bms-back/bms-system/src/main/resources/mapper/price/BmsProductMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/BmsProductMapper.xml index a1b75638..ce32e527 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/BmsProductMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/BmsProductMapper.xml @@ -207,6 +207,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + update product + set expire_date = null, + update_time = sysdate() + where product_code = #{productCode} + and expire_date = #{expireDate} + + update product set status = case diff --git a/bms-back/sql/price/product_remove_perm.sql b/bms-back/sql/price/product_remove_perm.sql new file mode 100644 index 00000000..c47b482b --- /dev/null +++ b/bms-back/sql/price/product_remove_perm.sql @@ -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'); diff --git a/bms-front/src/views/price/basic/product/index.vue b/bms-front/src/views/price/basic/product/index.vue index 5e72c748..8e2cb1cf 100644 --- a/bms-front/src/views/price/basic/product/index.vue +++ b/bms-front/src/views/price/basic/product/index.vue @@ -15,6 +15,7 @@ :search-columns="searchColumns" :query="queryList" row-key="productId" + :selection="true" > + + 批量删除 + 失效价格 @@ -152,9 +163,10 @@ import ProductExcelFileDrawer from './components/ProductExcelFileDrawer.vue' import ProductCountryDrawer from './components/ProductCountryDrawer.vue' import ProductSurchargeDrawer from './components/ProductSurchargeDrawer.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' +const { proxy } = getCurrentInstance() const { bms_product_effect_status } = useDict('bms_product_effect_status') const extraParams = { statusGroup: 'active' } @@ -225,11 +237,24 @@ const columns = [ const searchColumns = [ { 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) { - 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() { @@ -240,6 +265,48 @@ function handleUpdate(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) { if (command === 'country') { handleCountryImport()