feat: disable user messaging and retain公益 service guidance
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package com.danyang.match.platform.chat.config;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 产品当前不提供用户之间的即时聊天。
|
||||
*
|
||||
* <p>在 HTTP 和 WebSocket 握手进入 Controller/Handler 前统一拒绝,
|
||||
* 防止只隐藏小程序入口却留下可调用的通信通道。
|
||||
*/
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class ChatDisabledFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String path = request.getRequestURI();
|
||||
String context = request.getContextPath();
|
||||
if (context != null && !context.isEmpty() && path.startsWith(context)) {
|
||||
path = path.substring(context.length());
|
||||
}
|
||||
if (path.equals("/conversations") || path.startsWith("/conversations/")
|
||||
|| path.equals("/ws/chat") || path.startsWith("/ws/chat/")) {
|
||||
response.setStatus(HttpServletResponse.SC_GONE);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write(
|
||||
"{\"success\":false,\"code\":\"CHAT_DISABLED\","
|
||||
+ "\"message\":\"平台未提供用户之间的即时聊天功能\",\"data\":null}");
|
||||
return;
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
+4
-23
@@ -1,7 +1,5 @@
|
||||
package com.danyang.match.platform.match.controller;
|
||||
|
||||
import com.danyang.match.platform.chat.entity.Conversation;
|
||||
import com.danyang.match.platform.chat.service.ChatService;
|
||||
import com.danyang.match.platform.match.service.RelationService;
|
||||
import com.danyang.match.platform.match.service.RelationService.LikeResult;
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
@@ -11,7 +9,6 @@ import com.danyang.match.platform.shared.idempotency.Idempotent;
|
||||
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -22,18 +19,16 @@ import org.springframework.web.bind.annotation.*;
|
||||
* DELETE /api/v1/likes/{targetUserId} — A 取消 Like B
|
||||
*
|
||||
* 核心规则:INV-005 未 Mutual 不可聊天
|
||||
* 响应契约对齐 miniapp/types/index.d.ts 的 LikeResult:{ relationStatus, mutualMatch, matchedConversationId }
|
||||
* 响应返回关系状态和 Match 结果,不创建用户会话。
|
||||
*
|
||||
* 说明:原 /relations/{targetUserId}/like(2026-09-10 版)已被本控制器取代,调用方统一走 /likes。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/likes")
|
||||
@RequiredArgsConstructor
|
||||
public class LikeController {
|
||||
|
||||
private final RelationService relationService;
|
||||
private final ChatService chatService;
|
||||
|
||||
@Idempotent
|
||||
@PostMapping
|
||||
@@ -44,7 +39,7 @@ public class LikeController {
|
||||
return ApiResponse.error(ApiResponseCode.BAD_REQUEST, "targetUserId不能为空");
|
||||
}
|
||||
LikeResult result = relationService.like(principal.id(), req.targetUserId);
|
||||
return ApiResponse.ok(LikeResponse.from(result, ensureConversation(result, principal.id(), req.targetUserId)));
|
||||
return ApiResponse.ok(LikeResponse.from(result));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,20 +57,8 @@ public class LikeController {
|
||||
/**
|
||||
* Mutual 达成时确保会话存在,并返回会话 id —— 前端据此直接跳转聊天页。
|
||||
*
|
||||
* - getOrCreateConversation 幂等,不会重复建会话
|
||||
* - 建会话失败不应让 Like 整体失败(Like 已落库),故降级为 null,前端仅提示不跳转
|
||||
*/
|
||||
private String ensureConversation(LikeResult result, String userId, String targetUserId) {
|
||||
if (!result.mutual || result.match == null) return null;
|
||||
try {
|
||||
Conversation conv = chatService.getOrCreateConversation(result.match.getId(), userId, targetUserId);
|
||||
return conv.getId();
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("Mutual 达成但会话创建失败,已降级:user={}, target={}, matchId={}, reason={}",
|
||||
userId, targetUserId, result.match.getId(), e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// 当前产品不提供用户间会话;Match 成功后仅展示结果和公益服务入口。
|
||||
|
||||
// ==================== DTO ====================
|
||||
|
||||
@@ -89,16 +72,14 @@ public class LikeController {
|
||||
public static class LikeResponse {
|
||||
private String relationStatus;
|
||||
private boolean mutualMatch;
|
||||
private String matchedConversationId;
|
||||
private String matchId;
|
||||
|
||||
public static LikeResponse from(LikeResult r, String matchedConversationId) {
|
||||
public static LikeResponse from(LikeResult r) {
|
||||
LikeResponse resp = new LikeResponse();
|
||||
resp.relationStatus = r.mutual
|
||||
? Constants.RELATION_STATUS_MUTUAL_MATCH
|
||||
: Constants.RELATION_STATUS_LIKED;
|
||||
resp.mutualMatch = r.mutual;
|
||||
resp.matchedConversationId = matchedConversationId;
|
||||
resp.matchId = r.match != null ? r.match.getId() : null;
|
||||
return resp;
|
||||
}
|
||||
|
||||
+1
-21
@@ -251,16 +251,7 @@ public class RelationshipService {
|
||||
matchMapper.insert(match);
|
||||
}
|
||||
|
||||
// V1.1: 活动互选不自动创建Conversation,需双方确认后由confirmActivityInterest处理
|
||||
if (autoCreateConversation) {
|
||||
try {
|
||||
chatService.getOrCreateConversation(match.getId(), userId, targetUserId);
|
||||
log.info("Match后自动创建会话: matchId={}, users={}", match.getId(), userId + "," + targetUserId);
|
||||
} catch (Exception e) {
|
||||
log.warn("创建会话失败,Match已创建但聊天可能不可用: matchId={}, error={}",
|
||||
match.getId(), e.toString(), e);
|
||||
}
|
||||
}
|
||||
// 当前产品不提供用户间聊天;Match 仅用于结果判定和公益服务引导。
|
||||
return match;
|
||||
}
|
||||
|
||||
@@ -286,7 +277,6 @@ public class RelationshipService {
|
||||
result.put("matchReused", true);
|
||||
result.put("matchId", existingMatch.get().getId());
|
||||
result.put("status", existingMatch.get().getStatus());
|
||||
result.put("conversationAutoCreated", true);
|
||||
log.info("活动互选复用已有Match: userId={}, partnerId={}, matchId={}", userId, partnerId, existingMatch.get().getId());
|
||||
return result;
|
||||
}
|
||||
@@ -299,20 +289,10 @@ public class RelationshipService {
|
||||
// 3. 创建新 Match Episode(source=activity_mutual,不自动创建 Conversation)
|
||||
Match match = createMatch(userId, partnerId, Constants.MATCH_SOURCE_ACTIVITY_MUTUAL, false);
|
||||
|
||||
// V1.1 Phase 16 修复(Gate:互选确认后双方可沟通):双确认后必须开启会话。
|
||||
// 此前此处未建会话,仅靠首条消息惰性创建,导致「双方可沟通」的入口不可见。
|
||||
// 现显式开启,保证互选成功后聊天入口立即可见。
|
||||
try {
|
||||
chatService.getOrCreateConversation(match.getId(), match.getUserAId(), match.getUserBId());
|
||||
} catch (Exception e) {
|
||||
log.warn("活动互选开启会话失败(Match 已创建): matchId={}, error={}", match.getId(), e.toString(), e);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("matchCreated", true);
|
||||
result.put("matchId", match.getId());
|
||||
result.put("priorUnmatch", priorUnmatchExists);
|
||||
result.put("conversationAutoCreated", true);
|
||||
log.info("活动互选创建新Match Episode: userId={}, partnerId={}, priorUnmatch={}, matchId={}",
|
||||
userId, partnerId, priorUnmatchExists, match.getId());
|
||||
return result;
|
||||
|
||||
+3
-3
@@ -73,7 +73,7 @@ class ActivityMutualConsentServiceTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Gate: 双确认后形成 Match 并开启 Conversation(此前只打日志,Gate 失败)")
|
||||
void doubleConfirmCreatesMatchAndConversation() {
|
||||
void doubleConfirmCreatesMatchWithoutConversation() {
|
||||
// 第一阶段:建立互选同意记录(二阶段同意)
|
||||
consentService.createMutualConsent(activityId, userA, userB);
|
||||
|
||||
@@ -95,8 +95,8 @@ class ActivityMutualConsentServiceTest {
|
||||
assertEquals(Constants.MATCH_SOURCE_ACTIVITY_MUTUAL, match.getSource());
|
||||
|
||||
// 会话必须已开启(Gate:互选确认后双方可沟通)
|
||||
assertTrue(conversationMapper.findByMatchId(match.getId()).isPresent(),
|
||||
"双确认后必须已开启 Conversation");
|
||||
assertTrue(conversationMapper.findByMatchId(match.getId()).isEmpty(),
|
||||
"双确认后不得创建用户聊天 Conversation");
|
||||
|
||||
// 溯源字段:consent 记录应绑定 match_id
|
||||
ActivityMutualConsent consent = consentMapper.findByActivityPair(activityId, userA, userB).orElseThrow();
|
||||
|
||||
@@ -71,6 +71,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@AutoConfigureMockMvc
|
||||
@Disabled("用户端即时聊天已按产品方案下线")
|
||||
class ChatPhase09IntegrationTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
|
||||
+2
-1
@@ -94,6 +94,7 @@ class P0FixVerificationTest {
|
||||
// ==================== P0-1: Block不变量验证 ====================
|
||||
|
||||
@Test
|
||||
@Disabled("用户端即时聊天已按产品方案下线")
|
||||
@DisplayName("INV-003: Block后A不可向B发送消息")
|
||||
void testBlockPreventsChat() {
|
||||
relationshipService.like(userA, userB);
|
||||
@@ -213,7 +214,7 @@ class P0FixVerificationTest {
|
||||
var convs = conversationMapper.selectList(
|
||||
new LambdaQueryWrapper<com.danyang.match.platform.chat.entity.Conversation>()
|
||||
.eq(com.danyang.match.platform.chat.entity.Conversation::getMatchId, match.getId()));
|
||||
assertFalse(convs.isEmpty(), "Match后应自动创建Conversation");
|
||||
assertTrue(convs.isEmpty(), "Match 后不得自动创建 Conversation");
|
||||
}
|
||||
|
||||
// ==================== P1-1: Required全字段过滤验证 ====================
|
||||
|
||||
+3
-3
@@ -258,12 +258,12 @@ class MatchmakerActionGuardTest {
|
||||
// ==================== MM-ACTION-002:会话/关系开启无 operator 触达面 ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("MM-ACTION-002(负向): operator token 打会话开启端点 → 403(match/conversation 仅用户路径)")
|
||||
@DisplayName("MM-ACTION-002(负向): operator token 打已下线会话端点 → 410")
|
||||
void operatorCannotOpenConversationFromMatch() throws Exception {
|
||||
// 认证层即拒绝(hasRole("USER")),无需真实 match 存在
|
||||
// 用户间会话已全局下线,先于角色鉴权返回 CHAT_DISABLED。
|
||||
mockMvc.perform(post("/conversations/from-match/{matchId}", "no-such-match")
|
||||
.header("Authorization", "Bearer " + operatorToken))
|
||||
.andExpect(status().isForbidden());
|
||||
.andExpect(status().isGone());
|
||||
}
|
||||
|
||||
// ==================== MM-ACTION-003:must-confirm 语义锁定 ====================
|
||||
|
||||
+4
-4
@@ -237,7 +237,7 @@ class RelationshipProgressPhase12Test {
|
||||
// 旧 Match 有会话
|
||||
Long oldConvCount = conversationMapper.selectCount(
|
||||
new LambdaQueryWrapper<Conversation>().eq(Conversation::getMatchId, first.getId()));
|
||||
assertEquals(1L, oldConvCount, "首个 Match 应有 1 个会话");
|
||||
assertEquals(0L, oldConvCount, "首个 Match 不应有用户聊天会话");
|
||||
|
||||
// Unmatch
|
||||
Match m1 = matchMapper.findById(first.getId()).orElseThrow();
|
||||
@@ -255,10 +255,10 @@ class RelationshipProgressPhase12Test {
|
||||
new LambdaQueryWrapper<Conversation>().eq(Conversation::getMatchId, second.getId())),
|
||||
"新 Match 不应自动继承旧会话(旧聊天权限不恢复)");
|
||||
|
||||
// 且旧会话仍挂在旧 Match 上,不会被新 Match 复用
|
||||
assertEquals(1L, conversationMapper.selectCount(
|
||||
// 历史数据为空:本产品不再创建用户聊天会话
|
||||
assertEquals(0L, conversationMapper.selectCount(
|
||||
new LambdaQueryWrapper<Conversation>().eq(Conversation::getMatchId, first.getId())),
|
||||
"旧会话应仍属于旧 Match");
|
||||
"旧 Match 也不应存在用户聊天会话");
|
||||
}
|
||||
|
||||
// ==================== 拒绝 / 恢复 / 退出 ====================
|
||||
|
||||
Reference in New Issue
Block a user