feat(product): 商品图删除接通 + 至少保留一张
- 详情页编辑态的删除叉子原是纯 CSS ::after 装饰、没绑事件,且列表用的是 asset id 而非删除接口要的 ProductImage id —— 接通:编辑态点图真调删除接口、用 ProductImage id,非编辑态仍点击放大 - 商品至少保留一张图(与「创建必须带图」一致):后端删到最后一张返回 400;前端只剩一张时那张不显示 叉子 + 提示「商品至少保留一张图;要移除请删除整个商品」;封面无对应 ProductImage(如三视图采用为封面) 则只展示、不可删 - 加 2 个后端测试(多张可删 200 / 删最后一张 400) 真实接口验:两张删一张 200、再删最后一张 400 并保留一张;后端测试通过,tsc/build 通过。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f02c8053a1
commit
f9c52a171c
@@ -98,3 +98,37 @@ class ProductMaterialsTests(TestCase):
|
||||
self.assertEqual(len(data["project_packs"]), 1)
|
||||
self.assertEqual(data["project_packs"][0]["project_name"], "项目甲")
|
||||
self.assertEqual(len(data["project_packs"][0]["items"]), 2) # 场景 + 视频素材
|
||||
|
||||
|
||||
class ProductImageDeleteGuardTests(TestCase):
|
||||
"""商品至少保留一张图:删到最后一张则拒绝(要清空只能删整个商品)。"""
|
||||
|
||||
def setUp(self):
|
||||
from apps.assets.models import Asset, AssetFile
|
||||
from .models import ProductImage
|
||||
|
||||
self.user = User.objects.create_user(username="idg", password="x")
|
||||
self.team = Team.objects.create(name="IDG", owner=self.user)
|
||||
TeamMember.objects.create(team=self.team, user=self.user, role=TeamMember.Role.OWNER)
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(self.user)
|
||||
self.product = Product.objects.create(team=self.team, created_by=self.user, title="P")
|
||||
|
||||
def mk_img(i):
|
||||
a = Asset.objects.create(team=self.team, created_by=self.user, name=f"img{i}", asset_type="image", category="product_image")
|
||||
AssetFile.objects.create(asset=a, object_key=f"k{i}", bucket="b", content_type="image/png", size_bytes=1, is_primary=True)
|
||||
return ProductImage.objects.create(product=self.product, asset=a, sort_order=i, is_primary=(i == 0))
|
||||
|
||||
self.img0 = mk_img(0)
|
||||
self.img1 = mk_img(1)
|
||||
|
||||
def test_delete_when_multiple_ok(self):
|
||||
res = self.client.delete(f"/api/products/{self.product.id}/images/{self.img1.id}/")
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(self.product.images.count(), 1)
|
||||
|
||||
def test_delete_last_image_rejected(self):
|
||||
self.client.delete(f"/api/products/{self.product.id}/images/{self.img1.id}/") # 删到只剩一张
|
||||
res = self.client.delete(f"/api/products/{self.product.id}/images/{self.img0.id}/") # 再删最后一张
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertEqual(self.product.images.count(), 1) # 没删掉,仍保留一张
|
||||
|
||||
@@ -192,10 +192,16 @@ class ProductViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
|
||||
@action(detail=True, methods=["delete"], url_path=r"images/(?P<image_id>[^/.]+)")
|
||||
def delete_image(self, request, pk=None, image_id=None):
|
||||
"""移除商品图(删 ProductImage 关联,保留底层 Asset)。"""
|
||||
"""移除商品图(删 ProductImage 关联,保留底层 Asset)。
|
||||
商品至少保留一张图(与「创建必须带图」一致):删到最后一张则拒绝 —— 要清空只能删整个商品。"""
|
||||
product = self.get_object()
|
||||
deleted, _ = ProductImage.objects.filter(product=product, id=image_id).delete()
|
||||
if not deleted:
|
||||
if not ProductImage.objects.filter(product=product, id=image_id).exists():
|
||||
return Response({"detail": "image not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
if product.images.count() <= 1:
|
||||
return Response(
|
||||
{"detail": "商品至少保留一张图片,如需移除请删除整个商品(或先上传新图再删旧图)"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
ProductImage.objects.filter(product=product, id=image_id).delete()
|
||||
product.refresh_from_db()
|
||||
return Response(ProductSerializer(product).data, status=status.HTTP_200_OK)
|
||||
|
||||
Reference in New Issue
Block a user