|
|
@@ -1,38 +1,43 @@
|
|
|
package com.etotem.cfc.controller.ai;
|
|
|
|
|
|
import com.etotem.cfc.common.Result;
|
|
|
-import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.dto.AiGateResult;
|
|
|
+import com.etotem.cfc.enums.AiGateStatus;
|
|
|
import com.etotem.cfc.service.AiGateway;
|
|
|
import com.etotem.cfc.service.MembershipService;
|
|
|
-import com.etotem.cfc.service.SysConfigService;
|
|
|
+import com.etotem.cfc.service.PortraitService;
|
|
|
import com.etotem.cfc.service.UserService;
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
|
|
import java.lang.reflect.Field;
|
|
|
-import java.util.HashMap;
|
|
|
import java.util.LinkedHashMap;
|
|
|
import java.util.Map;
|
|
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
import static org.mockito.ArgumentMatchers.anyLong;
|
|
|
+import static org.mockito.ArgumentMatchers.anyMap;
|
|
|
+import static org.mockito.ArgumentMatchers.anyString;
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
import static org.mockito.Mockito.*;
|
|
|
|
|
|
/**
|
|
|
- * AI 教练会员付费开关单元测试(AIChatController § gate 逻辑)。
|
|
|
+ * AI 使用会员门禁单元测试(AIChatController,任务书 2026-11-02-ai-member-gate-design §3.1)。
|
|
|
*
|
|
|
- * 测试矩阵:
|
|
|
- * - 开关关闭 → 任何登录用户均可使用
|
|
|
- * - 开关开启 + FREE → 返回 46003 错误
|
|
|
- * - 开关开启 + BASIC → 返回 46003 错误
|
|
|
- * - 开关开启 + FAMILY → 放行
|
|
|
- * - 开关开启 + PREMIUM → 放行
|
|
|
- * - gate 状态接口正确返回 enabled / canUse / memberLevel / memberExpireTime
|
|
|
+ * <p>门禁判定本身在 MembershipService(见 MembershipServiceAiGateTest),
|
|
|
+ * 本测试聚焦控制器接线与错误码/响应结构:</p>
|
|
|
+ * <ul>
|
|
|
+ * <li>开关关闭(gate.enabled=false → ALLOW)→ 所有登录用户放行</li>
|
|
|
+ * <li>BASIC / FAMILY / PREMIUM 有效 → 放行</li>
|
|
|
+ * <li>FREE(UPGRADE_REQUIRED)→ 46003</li>
|
|
|
+ * <li>曾付费过期(MEMBER_EXPIRED)→ 46004</li>
|
|
|
+ * <li>/chat/send、/nutrition/send、/health-coach/send、/butler/send 同一门禁</li>
|
|
|
+ * <li>/api/ai/gate 返回 features 明细;/health-coach/gate 兼容保留</li>
|
|
|
+ * </ul>
|
|
|
*/
|
|
|
class AIChatControllerGateTest {
|
|
|
|
|
|
private AIChatController controller;
|
|
|
- private SysConfigService sysConfigService;
|
|
|
private MembershipService membershipService;
|
|
|
private UserService userService;
|
|
|
private AiGateway aiGateway;
|
|
|
@@ -40,17 +45,16 @@ class AIChatControllerGateTest {
|
|
|
@BeforeEach
|
|
|
void setUp() throws Exception {
|
|
|
controller = new AIChatController();
|
|
|
- sysConfigService = mock(SysConfigService.class);
|
|
|
membershipService = mock(MembershipService.class);
|
|
|
userService = mock(UserService.class);
|
|
|
aiGateway = mock(AiGateway.class);
|
|
|
- inject("sysConfigService", sysConfigService);
|
|
|
inject("membershipService", membershipService);
|
|
|
inject("userService", userService);
|
|
|
+ inject("portraitService", mock(PortraitService.class));
|
|
|
inject("aiGateway", aiGateway);
|
|
|
// aiGateway.chat 在放行路径会被调用,提供最小 stub
|
|
|
- when(aiGateway.chat(anyString(), anyLong(), anyString(), anyMap()))
|
|
|
- .thenReturn(new LinkedHashMap<>(Map.of("answer", "OK", "conversationId", "")));
|
|
|
+ when(aiGateway.chat(anyString(), anyLong(), any(), anyMap()))
|
|
|
+ .thenReturn(chatResponse());
|
|
|
}
|
|
|
|
|
|
private void inject(String name, Object v) throws Exception {
|
|
|
@@ -65,83 +69,220 @@ class AIChatControllerGateTest {
|
|
|
return p;
|
|
|
}
|
|
|
|
|
|
+ private Map<String, String> sendStringParams(String query) {
|
|
|
+ Map<String, String> p = new LinkedHashMap<>();
|
|
|
+ p.put("query", query);
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> chatResponse() {
|
|
|
+ Map<String, Object> resp = new LinkedHashMap<>();
|
|
|
+ resp.put("answer", "OK");
|
|
|
+ resp.put("conversationId", "");
|
|
|
+ return resp;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 构造门禁结果 */
|
|
|
+ private AiGateResult gate(boolean enabled, AiGateStatus status, boolean canUse,
|
|
|
+ boolean canViewHistory, String level) {
|
|
|
+ AiGateResult g = new AiGateResult();
|
|
|
+ g.setEnabled(enabled);
|
|
|
+ g.setStatus(status);
|
|
|
+ g.setCanUse(canUse);
|
|
|
+ g.setCanViewHistory(canViewHistory);
|
|
|
+ g.setMemberLevel(level);
|
|
|
+ g.setIsActive(status == AiGateStatus.ALLOW && enabled);
|
|
|
+ g.setDaysRemaining(0L);
|
|
|
+ g.setReason(status == AiGateStatus.ALLOW ? null : status.name());
|
|
|
+ return g;
|
|
|
+ }
|
|
|
+
|
|
|
+ private AiGateResult allow(String level) {
|
|
|
+ return gate(true, AiGateStatus.ALLOW, true, true, level);
|
|
|
+ }
|
|
|
+
|
|
|
+ private AiGateResult upgradeRequired() {
|
|
|
+ return gate(true, AiGateStatus.UPGRADE_REQUIRED, false, false, "FREE");
|
|
|
+ }
|
|
|
+
|
|
|
+ private AiGateResult expired() {
|
|
|
+ return gate(true, AiGateStatus.MEMBER_EXPIRED, false, true, "FREE");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── health-coach/send ──
|
|
|
+
|
|
|
@Test
|
|
|
void 开关关闭_FREE用户也可用() {
|
|
|
- when(sysConfigService.getValue("ai.coach.member_gate_enabled")).thenReturn("false");
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(gate(false, AiGateStatus.ALLOW, true, true, "FREE"));
|
|
|
Result<?> r = controller.sendHealthCoachMessage(1L, sendMessageParams("你好"));
|
|
|
assertEquals(200, r.getCode());
|
|
|
- // 放行后调用 aiGateway.chat
|
|
|
- verify(aiGateway).chat(anyString(), eq(1L), anyString(), anyMap());
|
|
|
+ verify(aiGateway).chat(anyString(), eq(1L), any(), anyMap());
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void 开关关闭_BASIC用户也可用() {
|
|
|
- when(sysConfigService.getValue("ai.coach.member_gate_enabled")).thenReturn("false");
|
|
|
+ void 开关开启_BASIC用户放行() {
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(allow("BASIC"));
|
|
|
Result<?> r = controller.sendHealthCoachMessage(1L, sendMessageParams("你好"));
|
|
|
assertEquals(200, r.getCode());
|
|
|
- verify(aiGateway).chat(anyString(), eq(1L), anyString(), anyMap());
|
|
|
+ verify(aiGateway).chat(anyString(), eq(1L), any(), anyMap());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 开关开启_FAMILY用户放行() {
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(allow("FAMILY"));
|
|
|
+ Result<?> r = controller.sendHealthCoachMessage(1L, sendMessageParams("你好"));
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ verify(aiGateway).chat(anyString(), eq(1L), any(), anyMap());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 开关开启_PREMIUM用户放行() {
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(allow("PREMIUM"));
|
|
|
+ Result<?> r = controller.sendHealthCoachMessage(1L, sendMessageParams("你好"));
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ verify(aiGateway).chat(anyString(), eq(1L), any(), anyMap());
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
void 开关开启_FREE用户被拦截返回46003() {
|
|
|
- when(sysConfigService.getValue("ai.coach.member_gate_enabled")).thenReturn("true");
|
|
|
- when(membershipService.getMemberLevel(1L)).thenReturn("FREE");
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(upgradeRequired());
|
|
|
Result<?> r = controller.sendHealthCoachMessage(1L, sendMessageParams("你好"));
|
|
|
assertEquals(46003, r.getCode());
|
|
|
- assertEquals("AI 教练为会员专享功能,请先开通会员", r.getMessage());
|
|
|
- verify(aiGateway, never()).chat(anyString(), anyLong(), anyString(), anyMap());
|
|
|
+ assertEquals("该功能为会员专享,请先开通会员", r.getMessage());
|
|
|
+ verify(aiGateway, never()).chat(anyString(), anyLong(), any(), anyMap());
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void 开关开启_BASIC用户被拦截返回46003() {
|
|
|
- when(sysConfigService.getValue("ai.coach.member_gate_enabled")).thenReturn("true");
|
|
|
- when(membershipService.getMemberLevel(1L)).thenReturn("BASIC");
|
|
|
+ void 开关开启_过期会员返回46004() {
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(expired());
|
|
|
Result<?> r = controller.sendHealthCoachMessage(1L, sendMessageParams("你好"));
|
|
|
+ assertEquals(46004, r.getCode());
|
|
|
+ assertEquals("会员已过期,请续费后继续使用", r.getMessage());
|
|
|
+ verify(aiGateway, never()).chat(anyString(), anyLong(), any(), anyMap());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── chat/send · nutrition/send · butler/send 同门禁 ──
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void chat_send_FREE被拦截返回46003() {
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(upgradeRequired());
|
|
|
+ Result<?> r = controller.sendMessage(1L, sendStringParams("你好"));
|
|
|
assertEquals(46003, r.getCode());
|
|
|
- verify(aiGateway, never()).chat(anyString(), anyLong(), anyString(), anyMap());
|
|
|
+ verifyNoInteractions(aiGateway);
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void 开关开启_FAMILY用户可用() {
|
|
|
- when(sysConfigService.getValue("ai.coach.member_gate_enabled")).thenReturn("true");
|
|
|
- when(membershipService.getMemberLevel(1L)).thenReturn("FAMILY");
|
|
|
- Result<?> r = controller.sendHealthCoachMessage(1L, sendMessageParams("你好"));
|
|
|
+ void chat_send_过期会员返回46004() {
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(expired());
|
|
|
+ Result<?> r = controller.sendMessage(1L, sendStringParams("你好"));
|
|
|
+ assertEquals(46004, r.getCode());
|
|
|
+ verifyNoInteractions(aiGateway);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void nutrition_send_过期会员返回46004() {
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(expired());
|
|
|
+ Map<String, String> p = sendStringParams("分析这份报告");
|
|
|
+ p.put("reportId", "99");
|
|
|
+ Result<?> r = controller.sendNutritionMessage(1L, p);
|
|
|
+ assertEquals(46004, r.getCode());
|
|
|
+ verifyNoInteractions(aiGateway);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void butler_send_FREE被拦截返回46003() {
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(upgradeRequired());
|
|
|
+ Result<?> r = controller.sendHealthButlerMessage(1L, sendMessageParams("帮我安排一下"));
|
|
|
+ assertEquals(46003, r.getCode());
|
|
|
+ verifyNoInteractions(aiGateway);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void butler_send_有效会员放行() {
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(allow("FAMILY"));
|
|
|
+ Result<?> r = controller.sendHealthButlerMessage(1L, sendMessageParams("帮我安排一下"));
|
|
|
assertEquals(200, r.getCode());
|
|
|
- verify(aiGateway).chat(anyString(), eq(1L), anyString(), anyMap());
|
|
|
+ verify(aiGateway).chat(anyString(), eq(1L), any(), anyMap());
|
|
|
}
|
|
|
|
|
|
+ // ── /api/ai/gate ──
|
|
|
+
|
|
|
@Test
|
|
|
- void 开关开启_PREMIUM用户可用() {
|
|
|
- when(sysConfigService.getValue("ai.coach.member_gate_enabled")).thenReturn("true");
|
|
|
- when(membershipService.getMemberLevel(1L)).thenReturn("PREMIUM");
|
|
|
- Result<?> r = controller.sendHealthCoachMessage(1L, sendMessageParams("你好"));
|
|
|
+ void gate接口_过期会员_返回全量features() {
|
|
|
+ AiGateResult g = expired();
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(g);
|
|
|
+ Result<AiGateResult> r = controller.aiGateStatus(1L);
|
|
|
assertEquals(200, r.getCode());
|
|
|
- verify(aiGateway).chat(anyString(), eq(1L), anyString(), anyMap());
|
|
|
+ AiGateResult d = r.getData();
|
|
|
+ assertEquals(AiGateStatus.MEMBER_EXPIRED, d.getStatus());
|
|
|
+ assertEquals(true, d.isEnabled());
|
|
|
+ assertEquals(false, d.isCanUse());
|
|
|
+ assertEquals(true, d.isCanViewHistory());
|
|
|
+ assertEquals("MEMBER_EXPIRED", d.getReason());
|
|
|
+
|
|
|
+ Map<String, AiGateResult.FeatureGate> features = d.getFeatures();
|
|
|
+ assertNotNull(features);
|
|
|
+ assertEquals(8, features.size());
|
|
|
+ assertFalse(features.get("chat").isCanUse());
|
|
|
+ assertFalse(features.get("nutrition").isCanUse());
|
|
|
+ assertFalse(features.get("healthCoach").isCanUse());
|
|
|
+ assertFalse(features.get("butler").isCanUse());
|
|
|
+ assertEquals("MEMBER_EXPIRED", features.get("chat").getReason());
|
|
|
+ // 不纳入门禁的 4 项固定放行(保留 FREE 引流钩子)
|
|
|
+ assertTrue(features.get("reportAnalyze").isCanUse());
|
|
|
+ assertTrue(features.get("emotion").isCanUse());
|
|
|
+ assertTrue(features.get("tongue").isCanUse());
|
|
|
+ assertTrue(features.get("diet").isCanUse());
|
|
|
+ assertNull(features.get("reportAnalyze").getReason());
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void configKeyMissing_defaultsToGateOn() {
|
|
|
- when(sysConfigService.getValue("ai.coach.member_gate_enabled")).thenReturn(null);
|
|
|
- when(membershipService.getMemberLevel(1L)).thenReturn("FREE");
|
|
|
- Result<?> r = controller.sendHealthCoachMessage(1L, sendMessageParams("你好"));
|
|
|
- assertEquals(46003, r.getCode());
|
|
|
+ void gate接口_有效会员_全部放行() {
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(allow("BASIC"));
|
|
|
+ Result<AiGateResult> r = controller.aiGateStatus(1L);
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ AiGateResult d = r.getData();
|
|
|
+ assertEquals(AiGateStatus.ALLOW, d.getStatus());
|
|
|
+ assertEquals(true, d.isCanUse());
|
|
|
+ assertTrue(d.getFeatures().get("chat").isCanUse());
|
|
|
+ assertNull(d.getFeatures().get("chat").getReason());
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void configKeyException_defaultsToGateOn() {
|
|
|
- when(sysConfigService.getValue("ai.coach.member_gate_enabled"))
|
|
|
- .thenThrow(new RuntimeException("config error"));
|
|
|
- when(membershipService.getMemberLevel(1L)).thenReturn("FREE");
|
|
|
- Result<?> r = controller.sendHealthCoachMessage(1L, sendMessageParams("你好"));
|
|
|
- assertEquals(46003, r.getCode());
|
|
|
+ void gate接口_开关关闭_全部放行() {
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(gate(false, AiGateStatus.ALLOW, true, true, "FREE"));
|
|
|
+ Result<AiGateResult> r = controller.aiGateStatus(1L);
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals(false, r.getData().isEnabled());
|
|
|
+ assertEquals(true, r.getData().isCanUse());
|
|
|
+ assertTrue(r.getData().getFeatures().get("butler").isCanUse());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void gate接口_JSON字段名符合契约() throws Exception {
|
|
|
+ AiGateResult g = expired();
|
|
|
+ g.setMemberExpireTime(new java.util.Date(1767225600000L));
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(g);
|
|
|
+ Result<AiGateResult> r = controller.aiGateStatus(1L);
|
|
|
+
|
|
|
+ String json = new com.fasterxml.jackson.databind.ObjectMapper()
|
|
|
+ .writeValueAsString(r.getData());
|
|
|
+ assertTrue(json.contains("\"status\":\"MEMBER_EXPIRED\""), json);
|
|
|
+ assertTrue(json.contains("\"enabled\":true"), json);
|
|
|
+ assertTrue(json.contains("\"canUse\":false"), json);
|
|
|
+ assertTrue(json.contains("\"canViewHistory\":true"), json);
|
|
|
+ assertTrue(json.contains("\"isActive\":false"), json);
|
|
|
+ assertTrue(json.contains("\"memberExpireTime\":"), json);
|
|
|
+ assertTrue(json.contains("\"reason\":\"MEMBER_EXPIRED\""), json);
|
|
|
+ assertTrue(json.contains("\"features\":{"), json);
|
|
|
+ assertTrue(json.contains("\"healthCoach\":{"), json);
|
|
|
}
|
|
|
|
|
|
- // ── gate 状态接口 ──
|
|
|
+ // ── 兼容接口 /health-coach/gate ──
|
|
|
|
|
|
@Test
|
|
|
void gateStatus_开关关闭返回enabledFalse_canUseTrue() {
|
|
|
- when(sysConfigService.getValue("ai.coach.member_gate_enabled")).thenReturn("false");
|
|
|
- when(userService.getUserInfo(1L)).thenReturn(buildUser(1L, "FREE", null));
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(gate(false, AiGateStatus.ALLOW, true, true, "FREE"));
|
|
|
Result<?> r = controller.aiCoachGateStatus(1L);
|
|
|
assertEquals(200, r.getCode());
|
|
|
Map<?, ?> d = (Map<?, ?>) r.getData();
|
|
|
@@ -152,9 +293,7 @@ class AIChatControllerGateTest {
|
|
|
|
|
|
@Test
|
|
|
void gateStatus_开关开启_FREE用户canUseFalse() {
|
|
|
- when(sysConfigService.getValue("ai.coach.member_gate_enabled")).thenReturn("true");
|
|
|
- when(userService.getUserInfo(1L)).thenReturn(buildUser(1L, "FREE", null));
|
|
|
- when(membershipService.getMemberLevel(1L)).thenReturn("FREE");
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(upgradeRequired());
|
|
|
Result<?> r = controller.aiCoachGateStatus(1L);
|
|
|
assertEquals(200, r.getCode());
|
|
|
Map<?, ?> d = (Map<?, ?>) r.getData();
|
|
|
@@ -165,10 +304,7 @@ class AIChatControllerGateTest {
|
|
|
|
|
|
@Test
|
|
|
void gateStatus_开关开启_FAMILY用户canUseTrue() {
|
|
|
- when(sysConfigService.getValue("ai.coach.member_gate_enabled")).thenReturn("true");
|
|
|
- User u = buildUser(1L, "FAMILY", null);
|
|
|
- when(userService.getUserInfo(1L)).thenReturn(u);
|
|
|
- when(membershipService.getMemberLevel(1L)).thenReturn("FAMILY");
|
|
|
+ when(membershipService.checkAiFeatureGate(1L)).thenReturn(allow("FAMILY"));
|
|
|
Result<?> r = controller.aiCoachGateStatus(1L);
|
|
|
assertEquals(200, r.getCode());
|
|
|
Map<?, ?> d = (Map<?, ?>) r.getData();
|
|
|
@@ -176,22 +312,4 @@ class AIChatControllerGateTest {
|
|
|
assertEquals(true, d.get("canUse"));
|
|
|
assertEquals("FAMILY", d.get("memberLevel"));
|
|
|
}
|
|
|
-
|
|
|
- @Test
|
|
|
- void gateStatus_anonymous返回canUseFalse() {
|
|
|
- when(sysConfigService.getValue("ai.coach.member_gate_enabled")).thenReturn("true");
|
|
|
- when(userService.getUserInfo(null)).thenReturn(null);
|
|
|
- Result<?> r = controller.aiCoachGateStatus(null);
|
|
|
- assertEquals(200, r.getCode());
|
|
|
- Map<?, ?> d = (Map<?, ?>) r.getData();
|
|
|
- assertEquals(false, d.get("canUse"));
|
|
|
- }
|
|
|
-
|
|
|
- private User buildUser(Long id, String level, java.util.Date expire) {
|
|
|
- User u = new User();
|
|
|
- u.setId(id);
|
|
|
- u.setMemberLevel(level);
|
|
|
- u.setMemberExpireTime(expire);
|
|
|
- return u;
|
|
|
- }
|
|
|
}
|