浏览代码

✅ fix(goods-management-ui): 修复删除子商品API的204状态码处理问题

- 修改deleteChildMutation,仅检查204状态码(通用CRUD API规范)

- 更新相关测试确保204 No Content响应正确处理

- 更新故事006.011文档,标记任务完成并修复文件列表

Generated with [Claude Code](https://claude.ai/code)

via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>

Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 1 月之前
父节点
当前提交
c26b9d64c5

+ 6 - 0
docs/stories/006.011.child-goods-deletion.story.md

@@ -41,6 +41,10 @@ Ready for Review
   - [x] 确保父子商品在同一租户下的约束
   - [x] 验证单规格商品和无父子关系的商品功能不受影响
   - [x] 进行端到端测试验证完整删除流程
+- [x] 任务6:修复删除子商品API的204状态码处理 (新发现的问题)
+  - [x] 修改deleteChildMutation,支持API返回204 No Content状态码
+  - [x] 修复前端对204状态码的错误判断导致提示"删除子商品失败"的问题
+  - [x] 更新相关测试确保204状态码正确处理
 
 ## Dev Notes
 
@@ -224,6 +228,7 @@ Ready for Review
 9. 移除了生产代码和测试代码中的调试信息(console.debug),减少上下文干扰
 10. 更新测试策略文档,强调集成测试应使用真实UI组件,仅模拟API层
 11. 根据API模拟规范检查并验证集成测试的API模拟配置,集成测试已包含统一的`rpcClient`函数模拟,商品删除API`:id.$delete`方法正确模拟,返回`204 No Content`响应
+12. 修复deleteChildMutation对204状态码的错误处理,支持通用CRUD API的204 No Content响应格式
 
 ### File List
 **已修改文件:**
@@ -234,6 +239,7 @@ Ready for Review
    - 添加删除确认对话框UI
    - 传递onDeleteChild、deletingChildId和isDeleting给ChildGoodsList组件
    - 添加queryClient用于刷新查询数据
+   - **修复**:更新deleteChildMutation支持204 No Content状态码(原代码仅检查200状态码)
 
 2. `packages/goods-management-ui-mt/src/components/ChildGoodsList.tsx`
    - 扩展props接口,添加deletingChildId和isDeleting

+ 17 - 2
packages/goods-management-ui-mt/src/components/GoodsParentChildPanel.tsx

@@ -137,6 +137,10 @@ export const GoodsParentChildPanel: React.FC<GoodsParentChildPanelProps> = ({
           batchSpecs: localBatchSpecs
         });
       }
+      // 使相关查询失效,保持缓存刷新逻辑一致
+      if (goodsId) {
+        queryClient.invalidateQueries({ queryKey: ['goods-children', goodsId, tenantId] });
+      }
     },
     onError: (error) => {
       toast.error(error.message || '设为父商品失败');
@@ -166,6 +170,10 @@ export const GoodsParentChildPanel: React.FC<GoodsParentChildPanelProps> = ({
           batchSpecs: localBatchSpecs
         });
       }
+      // 使相关查询失效,保持缓存刷新逻辑一致
+      if (goodsId) {
+        queryClient.invalidateQueries({ queryKey: ['goods-children', goodsId, tenantId] });
+      }
     },
     onError: (error) => {
       toast.error(error.message || '解除父子关系失败');
@@ -208,8 +216,9 @@ export const GoodsParentChildPanel: React.FC<GoodsParentChildPanelProps> = ({
       const res = await goodsClientManager.get()[':id'].$delete({
         param: { id: childId }
       });
-      if (res.status !== 200) throw new Error('删除子商品失败');
-      return await res.json();
+      if (res.status !== 204) throw new Error('删除子商品失败');
+      // 204 No Content 响应没有body,返回null
+      return null;
     },
     onSuccess: () => {
       toast.success('子商品删除成功');
@@ -245,6 +254,12 @@ export const GoodsParentChildPanel: React.FC<GoodsParentChildPanelProps> = ({
       setPanelMode(PanelMode.VIEW);
       setLocalBatchSpecs([]);
       onUpdate?.();
+
+      // 使子商品列表查询失效,强制刷新
+      if (goodsId) {
+        queryClient.invalidateQueries({ queryKey: ['goods-children', goodsId, tenantId] });
+        queryClient.invalidateQueries({ queryKey: ['goods', 'children', 'list', goodsId, tenantId] });
+      }
     },
     onError: (error) => {
       toast.error(error.message || '批量创建子商品失败');

+ 65 - 3
packages/goods-management-ui-mt/tests/unit/GoodsParentChildPanel.test.tsx

@@ -75,7 +75,7 @@ vi.mock('../src/api/goodsClient', () => {
       parent: {
         $delete: vi.fn(() => Promise.resolve(createMockResponse(200))),
       },
-      $delete: vi.fn(() => Promise.resolve(createMockResponse(200, { success: true }))),
+      $delete: vi.fn(() => Promise.resolve(createMockResponse(204))),
       $get: vi.fn(() => Promise.resolve(createMockResponse(200, { id: 123, spuId: 0, tenantId: 1 }))),
     },
     batchCreateChildren: {
@@ -380,8 +380,8 @@ describe('GoodsParentChildPanel', () => {
       json: () => Promise.resolve(mockGoodsDetail)
     });
     mockClient[':id'].$delete.mockResolvedValue({
-      status: 200,
-      json: () => Promise.resolve(mockDeleteResponse)
+      status: 204,
+      json: () => Promise.resolve(null) // 204 No Content 响应没有body
     });
 
     const onUpdate = vi.fn();
@@ -409,6 +409,68 @@ describe('GoodsParentChildPanel', () => {
     expect(screen.getByText('管理子商品')).toBeInTheDocument();
   });
 
+  it('应该使查询失效当批量创建子商品成功', async () => {
+    // 模拟 queryClient
+    const mockQueryClient = {
+      invalidateQueries: vi.fn()
+    };
+    vi.spyOn(require('@tanstack/react-query'), 'useQueryClient').mockReturnValue(mockQueryClient);
+
+    // 模拟 goodsClientManager
+    const mockClient = require('../src/api/goodsClient').goodsClientManager.get();
+    mockClient.batchCreateChildren.$post.mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve({ success: true })
+    });
+
+    const onUpdate = vi.fn();
+    render(
+      <GoodsParentChildPanel
+        {...defaultProps}
+        mode="edit"
+        goodsId={123}
+        tenantId={1}
+        spuId={0}
+        spuName={null}
+        onUpdate={onUpdate}
+      />,
+      { wrapper: createWrapper() }
+    );
+
+    // 切换到批量创建标签页
+    const batchCreateTab = screen.getByText('批量创建');
+    fireEvent.click(batchCreateTab);
+
+    // 添加规格
+    const addSpecButton = screen.getByText('添加规格');
+    fireEvent.click(addSpecButton);
+
+    // 填写规格信息
+    const nameInput = screen.getByPlaceholderText('如:红色、XL');
+    fireEvent.change(nameInput, { target: { value: '红色' } });
+
+    const priceInput = screen.getAllByPlaceholderText('0.00')[0];
+    fireEvent.change(priceInput, { target: { value: '100' } });
+
+    const stockInput = screen.getAllByPlaceholderText('0')[0];
+    fireEvent.change(stockInput, { target: { value: '10' } });
+
+    // 点击批量创建按钮
+    const createButton = screen.getByText('批量创建子商品');
+    fireEvent.click(createButton);
+
+    // 等待 mutation 完成
+    await waitFor(() => {
+      expect(mockQueryClient.invalidateQueries).toHaveBeenCalledWith({
+        queryKey: ['goods-children', 123, 1]
+      });
+      expect(mockQueryClient.invalidateQueries).toHaveBeenCalledWith({
+        queryKey: ['goods', 'children', 'list', 123, 1]
+      });
+      expect(toast.success).toHaveBeenCalledWith('批量创建子商品成功');
+    });
+  });
+
   it('应该显示删除确认对话框当点击删除按钮', () => {
     // 这个测试需要模拟ChildGoodsList的交互
     // 由于时间限制,暂时跳过