فهرست منبع

fix(backend): B-5 Result.error() 恒500结构级修复 — 新增400/403/404/409工厂方法 + Activity发布/审核调用点迁移

原提交 c6ad6c801 由 dev-lead5 于 2026-11-04 07:46 生成但从未 push 到远端(悬空对象,父提交 db761bbc9 在云端)。本提交将该修复 cherry-pick 到当前 origin/cfclub HEAD (2f3c73f45),消除 B-5 阻塞「冒烟通过率停45%」根因。

变更:Result.java 新增 badRequest/forbidden/notFound/conflict 工厂方法(仅新增零破坏);ActivityAdminService/ActivityService 参数缺失/无权限/不存在/冲突语义改走 4xx 工厂,不再兜 500。新增 ResultCodeFactoryTest 11 例全绿;ActivityServicePublishTest 6/6、ActivityAdminServiceTest 10/10 全绿。

验证:mvn -o test ResultCodeFactoryTest=11/11;Activity 两组 16/16。
Agnes Agent 1 هفته پیش
والد
کامیت
d7cf3346d9

+ 32 - 0
cfc-backend/src/main/java/com/etotem/cfc/common/Result.java

@@ -56,6 +56,38 @@ public class Result<T> implements Serializable {
         return result;
     }
 
+    /** 参数缺失/格式错误 → 400 */
+    public static <T> Result<T> badRequest(String message) {
+        Result<T> result = new Result<>();
+        result.setCode(400);
+        result.setMessage(message != null && !message.isEmpty() ? message : "请求参数错误");
+        return result;
+    }
+
+    /** 无权限/未授权 → 403 */
+    public static <T> Result<T> forbidden(String message) {
+        Result<T> result = new Result<>();
+        result.setCode(403);
+        result.setMessage(message != null && !message.isEmpty() ? message : "无权限访问");
+        return result;
+    }
+
+    /** 资源不存在 → 404 */
+    public static <T> Result<T> notFound(String message) {
+        Result<T> result = new Result<>();
+        result.setCode(404);
+        result.setMessage(message != null && !message.isEmpty() ? message : "资源不存在");
+        return result;
+    }
+
+    /** 状态冲突 → 409 */
+    public static <T> Result<T> conflict(String message) {
+        Result<T> result = new Result<>();
+        result.setCode(409);
+        result.setMessage(message != null && !message.isEmpty() ? message : "状态冲突");
+        return result;
+    }
+
     // Explicit accessors (Lombok fallback)
     public Integer getCode() { return code; }
     public void setCode(Integer code) { this.code = code; }

+ 6 - 6
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityAdminService.java

@@ -99,20 +99,20 @@ public class ActivityAdminService {
     public Result<String> review(Map<String, Object> params) {
         String action = (String) params.get("action");
         Long id = params.get("id") != null ? ParamUtils.getLong(params.get("id")) : null;
-        if (id == null) return Result.error("id不能为空");
-        if (action == null || action.isEmpty()) return Result.error("action不能为空");
+        if (id == null) return Result.badRequest("id不能为空");
+        if (action == null || action.isEmpty()) return Result.badRequest("action不能为空");
 
         Activity activity = activityMapper.selectById(id);
-        if (activity == null) return Result.error("活动不存在");
+        if (activity == null) return Result.notFound("活动不存在");
 
         if ("publish".equals(action)) {
             if (!"draft".equals(activity.getStatus())) {
-                return Result.error("仅草稿状态可发布");
+                return Result.conflict("仅草稿状态可发布");
             }
             // 审核通过时维度为必填
             String dimensionCode = (String) params.get("dimensionCode");
             if (dimensionCode == null || dimensionCode.trim().isEmpty()) {
-                return Result.error("审核通过时必须选择所属维度");
+                return Result.badRequest("审核通过时必须选择所属维度");
             }
             activity.setStatus("published");
             activity.setDimensionCode(dimensionCode.trim());
@@ -123,7 +123,7 @@ public class ActivityAdminService {
             activityMapper.deleteById(id);
             return Result.success("已驳回并删除");
         } else {
-            return Result.error("无效的action");
+            return Result.badRequest("无效的action");
         }
     }
 

+ 4 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityService.java

@@ -207,14 +207,14 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
     public Result<String> publish(Long id) {
         Activity activity = this.getById(id);
         if (activity == null) {
-            return Result.error("活动不存在");
+            return Result.notFound("活动不存在");
         }
         if (!"draft".equals(activity.getStatus())) {
-            return Result.error("仅草稿状态可发布");
+            return Result.conflict("仅草稿状态可发布");
         }
         // P1-2: 发布前校验维度代码,两路(直发/审核流)统一要求 dimensionCode 非空
         if (activity.getDimensionCode() == null || activity.getDimensionCode().trim().isEmpty()) {
-            return Result.error("请先设置活动维度(dimensionCode)");
+            return Result.badRequest("请先设置活动维度(dimensionCode)");
         }
         activity.setStatus("published");
         activity.setUpdatedAt(new Date());
@@ -225,7 +225,7 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
     public Result<String> end(Long id) {
         Activity activity = this.getById(id);
         if (activity == null) {
-            return Result.error("活动不存在");
+            return Result.notFound("活动不存在");
         }
         activity.setStatus("ended");
         activity.setUpdatedAt(new Date());

+ 101 - 0
cfc-backend/src/test/java/com/etotem/cfc/common/ResultCodeFactoryTest.java

@@ -0,0 +1,101 @@
+package com.etotem.cfc.common;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * B-5 契约单测:Result 4xx 工厂方法(错误语义可判定)。
+ *
+ * 覆盖:
+ * - badRequest → 400
+ * - forbidden  → 403
+ * - notFound   → 404
+ * - conflict   → 409
+ * - message 传入 / 默认两个分支
+ * - error(String) 保持 500 语义不变(防回归)
+ * - error(Integer,String) 保持透传不变(防回归)
+ */
+class ResultCodeFactoryTest {
+
+    @Test
+    void badRequest_sets400_withMessage() {
+        Result<String> result = Result.badRequest("参数缺失");
+        assertEquals(400, result.getCode());
+        assertEquals("参数缺失", result.getMessage());
+    }
+
+    @Test
+    void badRequest_sets400_withDefaultMessage_whenNull() {
+        Result<String> result = Result.badRequest(null);
+        assertEquals(400, result.getCode());
+        assertNotNull(result.getMessage(), "message 为空时应给默认文案");
+        assertFalse(result.getMessage().isEmpty());
+    }
+
+    @Test
+    void badRequest_sets400_withDefaultMessage_whenEmpty() {
+        Result<String> result = Result.badRequest("");
+        assertEquals(400, result.getCode());
+        assertFalse(result.getMessage().isEmpty(), "message 为空时应给默认文案");
+    }
+
+    @Test
+    void forbidden_sets403_withMessage() {
+        Result<String> result = Result.forbidden("无权限操作");
+        assertEquals(403, result.getCode());
+        assertEquals("无权限操作", result.getMessage());
+    }
+
+    @Test
+    void forbidden_sets403_withDefaultMessage_whenNull() {
+        Result<String> result = Result.forbidden(null);
+        assertEquals(403, result.getCode());
+        assertNotNull(result.getMessage(), "message 为空时应给默认文案");
+        assertFalse(result.getMessage().isEmpty());
+    }
+
+    @Test
+    void notFound_sets404_withMessage() {
+        Result<String> result = Result.notFound("活动不存在");
+        assertEquals(404, result.getCode());
+        assertEquals("活动不存在", result.getMessage());
+    }
+
+    @Test
+    void notFound_sets404_withDefaultMessage_whenNull() {
+        Result<String> result = Result.notFound(null);
+        assertEquals(404, result.getCode());
+        assertNotNull(result.getMessage(), "message 为空时应给默认文案");
+        assertFalse(result.getMessage().isEmpty());
+    }
+
+    @Test
+    void conflict_sets409_withMessage() {
+        Result<String> result = Result.conflict("仅草稿状态可发布");
+        assertEquals(409, result.getCode());
+        assertEquals("仅草稿状态可发布", result.getMessage());
+    }
+
+    @Test
+    void conflict_sets409_withDefaultMessage_whenNull() {
+        Result<String> result = Result.conflict(null);
+        assertEquals(409, result.getCode());
+        assertNotNull(result.getMessage(), "message 为空时应给默认文案");
+        assertFalse(result.getMessage().isEmpty());
+    }
+
+    @Test
+    void error_keeps500_semantics() {
+        Result<String> result = Result.error("系统内部错误");
+        assertEquals(500, result.getCode());
+        assertEquals("系统内部错误", result.getMessage());
+    }
+
+    @Test
+    void errorCodeMessage_passthrough() {
+        Result<String> result = Result.error(410, "该接口已废弃");
+        assertEquals(410, result.getCode());
+        assertEquals("该接口已废弃", result.getMessage());
+    }
+}

+ 7 - 6
cfc-backend/src/test/java/com/etotem/cfc/integration/service/ActivityAdminServiceTest.java

@@ -118,6 +118,7 @@ public class ActivityAdminServiceTest {
         Map<String, Object> params = new HashMap<>();
         params.put("action", "publish");
         params.put("id", 5L);
+        params.put("dimensionCode", "body");
         Result<String> result = activityAdminService.review(params);
 
         assertEquals(200, result.getCode());
@@ -152,8 +153,8 @@ public class ActivityAdminServiceTest {
         params.put("id", 7L);
         Result<String> result = activityAdminService.review(params);
 
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("仅草稿状态可发布"));
+        assertEquals(409, result.getCode());
+        assertEquals("仅草稿状态可发布", result.getMessage());
     }
 
     @Test
@@ -165,7 +166,7 @@ public class ActivityAdminServiceTest {
         params.put("id", 999L);
         Result<String> result = activityAdminService.review(params);
 
-        assertEquals(500, result.getCode());
+        assertEquals(404, result.getCode());
         assertTrue(result.getMessage().contains("活动不存在"));
     }
 
@@ -179,7 +180,7 @@ public class ActivityAdminServiceTest {
         params.put("id", 8L);
         Result<String> result = activityAdminService.review(params);
 
-        assertEquals(500, result.getCode());
+        assertEquals(400, result.getCode());
         assertTrue(result.getMessage().contains("无效的action"));
     }
 
@@ -189,7 +190,7 @@ public class ActivityAdminServiceTest {
         params.put("action", "publish");
         Result<String> result = activityAdminService.review(params);
 
-        assertEquals(500, result.getCode());
+        assertEquals(400, result.getCode());
         assertTrue(result.getMessage().contains("id不能为空"));
     }
 
@@ -199,7 +200,7 @@ public class ActivityAdminServiceTest {
         params.put("id", 5L);
         Result<String> result = activityAdminService.review(params);
 
-        assertEquals(500, result.getCode());
+        assertEquals(400, result.getCode());
         assertTrue(result.getMessage().contains("action不能为空"));
     }
 }

+ 7 - 6
cfc-backend/src/test/java/com/etotem/cfc/integration/service/ActivityServicePublishTest.java

@@ -48,6 +48,7 @@ public class ActivityServicePublishTest {
     @Test
     public void publish_draft_success() throws Exception {
         Activity activity = mockActivity(1L, "draft");
+        activity.setDimensionCode("body");
         ActivityService spy = org.mockito.Mockito.spy(service);
         org.mockito.Mockito.doReturn(activity).when(spy).getById(1L);
         org.mockito.Mockito.doReturn(true).when(spy).updateById(activity);
@@ -62,26 +63,26 @@ public class ActivityServicePublishTest {
     }
 
     @Test
-    public void publish_notFound_returnsError() throws Exception {
+    public void publish_notFound_returnsNotFound() throws Exception {
         ActivityService spy = org.mockito.Mockito.spy(service);
         org.mockito.Mockito.doReturn(null).when(spy).getById(999L);
 
         Result<String> result = spy.publish(999L);
 
-        assertEquals(500, result.getCode());
+        assertEquals(404, result.getCode());
         assertEquals("活动不存在", result.getMessage());
         org.mockito.Mockito.verify(spy, org.mockito.Mockito.never()).updateById(org.mockito.Mockito.any());
     }
 
     @Test
-    public void publish_notDraft_returnsError() throws Exception {
+    public void publish_notDraft_returnsConflict() throws Exception {
         Activity activity = mockActivity(1L, "published");
         ActivityService spy = org.mockito.Mockito.spy(service);
         org.mockito.Mockito.doReturn(activity).when(spy).getById(1L);
 
         Result<String> result = spy.publish(1L);
 
-        assertEquals(500, result.getCode());
+        assertEquals(409, result.getCode());
         assertEquals("仅草稿状态可发布", result.getMessage());
         org.mockito.Mockito.verify(spy, org.mockito.Mockito.never()).updateById(org.mockito.Mockito.any());
     }
@@ -104,13 +105,13 @@ public class ActivityServicePublishTest {
     }
 
     @Test
-    public void end_notFound_returnsError() throws Exception {
+    public void end_notFound_returnsNotFound() throws Exception {
         ActivityService spy = org.mockito.Mockito.spy(service);
         org.mockito.Mockito.doReturn(null).when(spy).getById(999L);
 
         Result<String> result = spy.end(999L);
 
-        assertEquals(500, result.getCode());
+        assertEquals(404, result.getCode());
         assertEquals("活动不存在", result.getMessage());
     }