feat(admin): 最低价ASIN列表操作列新增整条删除按钮——后端DELETE /skip-price-asins/{id}(权限校验+日志),前端确认弹窗+删空页回退,补单测与对齐测试
This commit is contained in:
@@ -8,7 +8,7 @@ import { createSkipPriceAsin, fetchSkipPriceList } from './skip-price-api.ts'
|
|||||||
import { skipPriceDisplayRows, type SkipPriceItem } from './skip-price-model.ts'
|
import { skipPriceDisplayRows, type SkipPriceItem } from './skip-price-model.ts'
|
||||||
import { asinCountryLabel } from './asin-country.ts'
|
import { asinCountryLabel } from './asin-country.ts'
|
||||||
import { createSkipPriceFilterState, toSkipPriceParams, type SkipPriceFilterState } from './skip-price-filter.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 { fetchSkipPriceDeleteImportProgress, fetchSkipPriceImportProgress, startSkipPriceDeleteImport, startSkipPriceImport } from './skip-price-import-api.ts'
|
||||||
import { isAllowedExcelImportFile } from './import-progress-model.ts'
|
import { isAllowedExcelImportFile } from './import-progress-model.ts'
|
||||||
import { fetchShopNamesByGroup } from './query-asin-api.ts'
|
import { fetchShopNamesByGroup } from './query-asin-api.ts'
|
||||||
@@ -204,6 +204,30 @@ async function saveConfig(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 整条删除 ----
|
||||||
|
async function removeRow(item: SkipPriceItem): Promise<void> {
|
||||||
|
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 必填) ----
|
// ---- 导入添加 / 删除导入(group_id 必填) ----
|
||||||
type ImportMode = 'add' | 'delete'
|
type ImportMode = 'add' | 'delete'
|
||||||
const importVisible = ref(false)
|
const importVisible = ref(false)
|
||||||
@@ -433,6 +457,7 @@ onMounted(() => {
|
|||||||
<template v-if="row.isFirst">
|
<template v-if="row.isFirst">
|
||||||
<td :rowspan="row.rowspan" class="asin-col-actions">
|
<td :rowspan="row.rowspan" class="asin-col-actions">
|
||||||
<button class="btn btn-sm" type="button" @click="openConfig(row.item as SkipPriceItem)">配置</button>
|
<button class="btn btn-sm" type="button" @click="openConfig(row.item as SkipPriceItem)">配置</button>
|
||||||
|
<button class="btn btn-sm btn-danger" type="button" @click="removeRow(row.item as SkipPriceItem)">删除</button>
|
||||||
</td>
|
</td>
|
||||||
</template>
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -21,3 +21,8 @@ export async function updateSkipPriceCountry(
|
|||||||
export async function deleteSkipPriceCountry(id: number, country: string): Promise<void> {
|
export async function deleteSkipPriceCountry(id: number, country: string): Promise<void> {
|
||||||
await http.delete<unknown>(countryUrl(id, country))
|
await http.delete<unknown>(countryUrl(id, country))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 删除整条最低价 ASIN 记录(该店铺全部站点):DELETE /api/admin/skip-price-asins/{id}。 */
|
||||||
|
export async function deleteSkipPriceAsin(id: number): Promise<void> {
|
||||||
|
await http.delete<unknown>(`${SKIP_PRICE_DETAIL_ENDPOINT}/${id}`)
|
||||||
|
}
|
||||||
|
|||||||
@@ -97,3 +97,17 @@ test('align_skip_price_filter_price_range_same_row', () => {
|
|||||||
assert.match(page, /placeholder="最高价 ≤"/, '上限占位文案(旧版)')
|
assert.match(page, /placeholder="最高价 ≤"/, '上限占位文案(旧版)')
|
||||||
assert.ok(!page.includes('label="最低价 ≥"'), '不再有独立的最低价 ≥ 筛选项')
|
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\}/, '删除端点指向整条记录')
|
||||||
|
})
|
||||||
|
|||||||
+10
@@ -165,6 +165,16 @@ public class SkipPriceAsinController {
|
|||||||
isSuper(operator)));
|
isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Operation(summary = "删除整条跳过跟价 ASIN 记录", description = "按主键删除店铺的全部站点 ASIN 与最低价配置。")
|
||||||
|
public ApiResponse<Void> 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}")
|
@DeleteMapping("/{id}/countries/{country}")
|
||||||
@Operation(summary = "删除指定国家的跳过跟价 ASIN")
|
@Operation(summary = "删除指定国家的跳过跟价 ASIN")
|
||||||
public ApiResponse<Void> deleteCountry(
|
public ApiResponse<Void> deleteCountry(
|
||||||
|
|||||||
+10
@@ -336,6 +336,16 @@ public class SkipPriceAsinService {
|
|||||||
skipPriceAsinMapper.updateById(entity);
|
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
|
@Transactional
|
||||||
public SkipPriceAsinItemVo updateCountry(Long id, String country, String asin, BigDecimal minimumPrice,
|
public SkipPriceAsinItemVo updateCountry(Long id, String country, String asin, BigDecimal minimumPrice,
|
||||||
Long operatorId, boolean superAdmin) {
|
Long operatorId, boolean superAdmin) {
|
||||||
|
|||||||
+27
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||||
import org.apache.ibatis.session.Configuration;
|
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.ShopManageMapper;
|
||||||
import com.nanri.aiimage.modules.shopkey.mapper.SkipPriceAsinMapper;
|
import com.nanri.aiimage.modules.shopkey.mapper.SkipPriceAsinMapper;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest;
|
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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
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.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -179,6 +181,31 @@ class SkipPriceAsinServiceTest {
|
|||||||
assertEquals(new BigDecimal("14.00"), existing.getMinimumPriceUk());
|
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() {
|
private ShopManageGroupEntity group() {
|
||||||
ShopManageGroupEntity group = new ShopManageGroupEntity();
|
ShopManageGroupEntity group = new ShopManageGroupEntity();
|
||||||
group.setId(10L);
|
group.setId(10L);
|
||||||
|
|||||||
Reference in New Issue
Block a user