diff --git a/admin-frontend-vue/src/pages/asin/SkipPricePage.vue b/admin-frontend-vue/src/pages/asin/SkipPricePage.vue index dcce3b2c..48c1c0e1 100644 --- a/admin-frontend-vue/src/pages/asin/SkipPricePage.vue +++ b/admin-frontend-vue/src/pages/asin/SkipPricePage.vue @@ -8,7 +8,7 @@ import { createSkipPriceAsin, fetchSkipPriceList } from './skip-price-api.ts' import { skipPriceDisplayRows, type SkipPriceItem } from './skip-price-model.ts' import { asinCountryLabel } from './asin-country.ts' import { createSkipPriceFilterState, toSkipPriceParams, type SkipPriceFilterState } from './skip-price-filter.ts' -import { deleteSkipPriceCountry, updateSkipPriceCountry } from './skip-price-detail-api.ts' +import { deleteSkipPriceAsin, deleteSkipPriceCountry, updateSkipPriceCountry } from './skip-price-detail-api.ts' import { fetchSkipPriceDeleteImportProgress, fetchSkipPriceImportProgress, startSkipPriceDeleteImport, startSkipPriceImport } from './skip-price-import-api.ts' import { isAllowedExcelImportFile } from './import-progress-model.ts' import { fetchShopNamesByGroup } from './query-asin-api.ts' @@ -204,6 +204,30 @@ async function saveConfig(): Promise { } } +// ---- 整条删除 ---- +async function removeRow(item: SkipPriceItem): Promise { + try { + await ElMessageBox.confirm( + `确定删除「${item.shopName}」${item.groupName ? `(${item.groupName})` : ''}全部站点的最低价 ASIN 配置吗?`, + '删除确认', + { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }, + ) + } catch { + return + } + try { + await deleteSkipPriceAsin(item.id) + ElMessage.success('删除成功') + // 删除后当前页若已空且非首页,回退一页避免展示空页。 + if (rows.value.length === 1 && page.value > 1) { + page.value -= 1 + } + await load() + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : '删除失败') + } +} + // ---- 导入添加 / 删除导入(group_id 必填) ---- type ImportMode = 'add' | 'delete' const importVisible = ref(false) @@ -433,6 +457,7 @@ onMounted(() => { diff --git a/admin-frontend-vue/src/pages/asin/skip-price-detail-api.ts b/admin-frontend-vue/src/pages/asin/skip-price-detail-api.ts index 4c232a70..70b9635b 100644 --- a/admin-frontend-vue/src/pages/asin/skip-price-detail-api.ts +++ b/admin-frontend-vue/src/pages/asin/skip-price-detail-api.ts @@ -21,3 +21,8 @@ export async function updateSkipPriceCountry( export async function deleteSkipPriceCountry(id: number, country: string): Promise { await http.delete(countryUrl(id, country)) } + +/** 删除整条最低价 ASIN 记录(该店铺全部站点):DELETE /api/admin/skip-price-asins/{id}。 */ +export async function deleteSkipPriceAsin(id: number): Promise { + await http.delete(`${SKIP_PRICE_DETAIL_ENDPOINT}/${id}`) +} diff --git a/admin-frontend-vue/tests/align-skip-price.test.ts b/admin-frontend-vue/tests/align-skip-price.test.ts index b5d335d5..a7c2c0cf 100644 --- a/admin-frontend-vue/tests/align-skip-price.test.ts +++ b/admin-frontend-vue/tests/align-skip-price.test.ts @@ -97,3 +97,17 @@ test('align_skip_price_filter_price_range_same_row', () => { assert.match(page, /placeholder="最高价 ≤"/, '上限占位文案(旧版)') assert.ok(!page.includes('label="最低价 ≥"'), '不再有独立的最低价 ≥ 筛选项') }) + +test('align_skip_price_action_delete_button_wiring', () => { + // 操作列新增整条删除:确认弹窗 + DELETE /skip-price-asins/{id} + 删空页回退。 + const page = readSource('src/pages/asin/SkipPricePage.vue') + assert.match(page, /btn-danger/, '删除按钮使用危险样式') + assert.match(page, /@click="removeRow/, '按钮绑定整条删除') + assert.match(page, /ElMessageBox\.confirm/, '删除前二次确认') + assert.match(page, /确定删除「/, '确认文案含店铺名') + assert.match(page, /deleteSkipPriceAsin\(item\.id\)/, '调用整条删除 API') + assert.match(page, /page\.value -= 1/, '删空当前页后回退一页') + const api = readSource('src/pages/asin/skip-price-detail-api.ts') + assert.match(api, /deleteSkipPriceAsin/, '导出整条删除函数') + assert.match(api, /SKIP_PRICE_DETAIL_ENDPOINT}\/\$\{id\}/, '删除端点指向整条记录') +}) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/SkipPriceAsinController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/SkipPriceAsinController.java index eae39965..de4365c4 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/SkipPriceAsinController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/SkipPriceAsinController.java @@ -165,6 +165,16 @@ public class SkipPriceAsinController { isSuper(operator))); } + @DeleteMapping("/{id}") + @Operation(summary = "删除整条跳过跟价 ASIN 记录", description = "按主键删除店铺的全部站点 ASIN 与最低价配置。") + public ApiResponse delete( + HttpServletRequest request, + @Parameter(description = "主键 ID", required = true) @PathVariable Long id) { + AdminUserEntity operator = requireOperator(request); + skipPriceAsinService.delete(id, operator.getId(), isSuper(operator)); + return ApiResponse.success("删除成功", null); + } + @DeleteMapping("/{id}/countries/{country}") @Operation(summary = "删除指定国家的跳过跟价 ASIN") public ApiResponse deleteCountry( diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinService.java index c99268d2..66a40ec4 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinService.java @@ -336,6 +336,16 @@ public class SkipPriceAsinService { skipPriceAsinMapper.updateById(entity); } + /** 删除整条最低价 ASIN 记录(该店铺全部站点的配置)。 */ + @Transactional + public void delete(Long id, Long operatorId, boolean superAdmin) { + SkipPriceAsinEntity entity = getById(id); + validateGroupAccess(entity, operatorId, superAdmin); + skipPriceAsinMapper.deleteById(entity.getId()); + log.info("[skip-price-asin] delete record id={} shopName={} groupId={} operatorId={}", + entity.getId(), entity.getShopName(), entity.getGroupId(), operatorId); + } + @Transactional public SkipPriceAsinItemVo updateCountry(Long id, String country, String asin, BigDecimal minimumPrice, Long operatorId, boolean superAdmin) { diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinServiceTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinServiceTest.java index ed020792..f4e2fcf6 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinServiceTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinServiceTest.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.apache.ibatis.session.Configuration; +import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.modules.shopkey.mapper.ShopManageMapper; import com.nanri.aiimage.modules.shopkey.mapper.SkipPriceAsinMapper; import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest; @@ -34,6 +35,7 @@ import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -179,6 +181,31 @@ class SkipPriceAsinServiceTest { assertEquals(new BigDecimal("14.00"), existing.getMinimumPriceUk()); } + @Test + void deleteRemovesWholeRecordAfterGroupAccessCheck() { + SkipPriceAsinEntity entity = new SkipPriceAsinEntity(); + entity.setId(100L); + entity.setGroupId(10L); + entity.setShopName("shop-a"); + entity.setAsinDe("B0DE1"); + when(skipPriceAsinMapper.selectById(100L)).thenReturn(entity); + when(shopManageGroupService.getAccessibleById(10L, 7L, true)).thenReturn(group()); + + service.delete(100L, 7L, true); + + verify(skipPriceAsinMapper).deleteById((java.io.Serializable) 100L); + } + + @Test + void deleteRejectsUnknownRecord() { + when(skipPriceAsinMapper.selectById(999L)).thenReturn(null); + + BusinessException exception = assertThrows(BusinessException.class, () -> service.delete(999L, 7L, true)); + + assertEquals("记录不存在", exception.getMessage()); + verify(skipPriceAsinMapper, never()).deleteById(any(java.io.Serializable.class)); + } + private ShopManageGroupEntity group() { ShopManageGroupEntity group = new ShopManageGroupEntity(); group.setId(10L);