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 也不应存在用户聊天会话");
|
||||
}
|
||||
|
||||
// ==================== 拒绝 / 恢复 / 退出 ====================
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# 用户端无即时通讯边界(2026-10-15)
|
||||
|
||||
## 已确认范围
|
||||
|
||||
- 小程序全流程不展示微信群二维码,也不展示或交换手机号、微信号、QQ 等个人联系方式。
|
||||
- 小程序不提供用户之间的私信、聊天、会话列表或 WebSocket 双向通讯。
|
||||
- 双向意向匹配只产生平台系统通知,并引导用户查看公益活动、公益服务站和公益红娘服务。
|
||||
- 活动签到二维码属于活动现场核验,不属于群二维码,继续保留。
|
||||
|
||||
## 实施边界
|
||||
|
||||
- Like 和活动互选不再创建新 Conversation,不返回 `matchedConversationId`。
|
||||
- 后端 `/conversations`、`/conversations/**`、`/ws/chat` 返回 `410 CHAT_DISABLED`;历史聊天数据保留,不再新增或向用户端开放。
|
||||
- Nginx `/ws/` 直接返回 `410 CHAT_DISABLED`,避免网关继续代理聊天连接。
|
||||
- 小程序不注册消息 Tab、消息页和聊天页;历史源码暂保留用于数据迁移和回归兼容,但不在运行路由中暴露。
|
||||
|
||||
## 验收
|
||||
|
||||
- `tests/miniapp/no_user_im_contract_test.py`:通过。
|
||||
- 仍需在后端容器完成完整 `mvn test -B`,并验证运行中的 REST 聊天接口返回 410。
|
||||
@@ -899,3 +899,8 @@ docker run --rm --dns 223.5.5.5 --dns 114.114.114.114 `
|
||||
- 后端镜像已按当前源码重建成功;健康检查通过,未认证通知接口返回 401。
|
||||
- 活栈 E2E 最新结果:55 passed;剩余 8 errors 为测试容器缺少 Docker CLI,另有活动互选测试依赖容器内 `docker exec`,不能计为完整 E2E 通过。
|
||||
- 证据:`.mimosa/core-e2e-postgres-test.log`。
|
||||
|
||||
## 2026-10-15 无用户 IM 改造
|
||||
- 已按确认方案关闭小程序用户私信/聊天入口,不展示微信群二维码。
|
||||
- 匹配成功保留平台系统通知,并引导公益活动、服务站、红娘服务。
|
||||
- 后端与 Nginx 聊天入口返回 CHAT_DISABLED/410;完整 Maven 回归与运行态验收待本轮完成。
|
||||
|
||||
@@ -147,16 +147,10 @@ http {
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# WebSocket proxy
|
||||
# 用户间即时聊天已下线,避免网关继续暴露 WebSocket 入口。
|
||||
location /ws/ {
|
||||
limit_req zone=ws burst=20 nodelay;
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_read_timeout 3600s;
|
||||
default_type application/json;
|
||||
return 410 '{"success":false,"code":"CHAT_DISABLED","message":"平台未提供用户之间的即时聊天功能","data":null}';
|
||||
}
|
||||
|
||||
# Auth endpoints (strict rate limiting)
|
||||
|
||||
@@ -68,17 +68,10 @@ http {
|
||||
add_header Pragma "no-cache";
|
||||
}
|
||||
|
||||
# WebSocket proxy
|
||||
# 用户间即时聊天已下线,避免网关继续暴露 WebSocket 入口。
|
||||
location /ws/ {
|
||||
proxy_pass $backend_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 3600s;
|
||||
default_type application/json;
|
||||
return 410 '{"success":false,"code":"CHAT_DISABLED","message":"平台未提供用户之间的即时聊天功能","data":null}';
|
||||
}
|
||||
|
||||
# Auth endpoints - stricter rate limit
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/discover/discover",
|
||||
"pages/messages/messages",
|
||||
"pages/activity/activity",
|
||||
"pages/mine/mine",
|
||||
"pages/login/login",
|
||||
@@ -23,7 +22,6 @@
|
||||
"pages/review-rejected/review-rejected",
|
||||
"pages/profile-detail/profile-detail",
|
||||
"pages/fate-center/fate-center",
|
||||
"pages/chat/chat",
|
||||
"pages/activity-detail/activity-detail",
|
||||
"pages/activity-register/activity-register",
|
||||
"pages/my-activities/my-activities",
|
||||
@@ -97,12 +95,6 @@
|
||||
"iconPath": "assets/tabbar/discover.png",
|
||||
"selectedIconPath": "assets/tabbar/discover-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/messages/messages",
|
||||
"text": "消息",
|
||||
"iconPath": "assets/tabbar/message.png",
|
||||
"selectedIconPath": "assets/tabbar/message-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/activity/activity",
|
||||
"text": "活动",
|
||||
|
||||
@@ -173,7 +173,7 @@ exports.ERROR_USER_MESSAGE = {
|
||||
[exports.BUSINESS_ERROR_CODE.PROFILE_NOT_APPROVED]: '当前资料尚未通过审核',
|
||||
[exports.BUSINESS_ERROR_CODE.ACCOUNT_LIMITED]: '账号已被限制,详情请查看账号状态',
|
||||
[exports.BUSINESS_ERROR_CODE.TARGET_UNAVAILABLE]: '该用户当前不可查看或联系',
|
||||
[exports.BUSINESS_ERROR_CODE.MATCH_REQUIRED]: '双方互相喜欢后才能开始聊天',
|
||||
[exports.BUSINESS_ERROR_CODE.MATCH_REQUIRED]: '当前平台未提供用户间即时聊天功能',
|
||||
[exports.BUSINESS_ERROR_CODE.ACTIVITY_FULL]: '本次活动报名人数已满',
|
||||
[exports.BUSINESS_ERROR_CODE.ACTIVITY_CLOSED]: '本次活动报名已结束',
|
||||
[exports.BUSINESS_ERROR_CODE.ALREADY_APPLIED]: '你已经提交过申请',
|
||||
|
||||
+1
-9
@@ -2,7 +2,6 @@
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/discover/discover",
|
||||
"pages/messages/messages",
|
||||
"pages/activity/activity",
|
||||
"pages/mine/mine",
|
||||
"pages/login/login",
|
||||
@@ -20,7 +19,6 @@
|
||||
"pages/review-rejected/review-rejected",
|
||||
"pages/profile-detail/profile-detail",
|
||||
"pages/fate-center/fate-center",
|
||||
"pages/chat/chat",
|
||||
"pages/activity-detail/activity-detail",
|
||||
"pages/activity-register/activity-register",
|
||||
"pages/my-activities/my-activities",
|
||||
@@ -68,12 +66,6 @@
|
||||
"iconPath": "assets/tabbar/discover.png",
|
||||
"selectedIconPath": "assets/tabbar/discover-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/messages/messages",
|
||||
"text": "消息",
|
||||
"iconPath": "assets/tabbar/message.png",
|
||||
"selectedIconPath": "assets/tabbar/message-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/activity/activity",
|
||||
"text": "活动",
|
||||
@@ -101,4 +93,4 @@
|
||||
}
|
||||
},
|
||||
"sitemapLocation": "sitemap.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +50,4 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
startChat(userId) {
|
||||
wx.navigateTo({
|
||||
url: `/pages/chat/chat?userId=${userId}&activityId=${this.data.activityId}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<text class="match-meta">{{item.age}}岁 · {{item.locationName || ''}}</text>
|
||||
</view>
|
||||
<view class="match-actions" catchtap>
|
||||
<button class="btn-chat" bindtap="startChat" data-user-id="{{item.targetUserId}}">聊天</button>
|
||||
<text class="match-service-hint">可查看公益服务</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -18,8 +18,4 @@ Page({
|
||||
} catch (e) { console.error(e); this.setData({ loading: false }); }
|
||||
},
|
||||
|
||||
openChat(e: any) {
|
||||
const partnerId = e.currentTarget.dataset.partnerid;
|
||||
wx.navigateTo({ url: `/pages/chat/chat?partnerId=${partnerId}` });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<button class="btn-waiting" disabled>等待对方确认</button>
|
||||
</view>
|
||||
<view class="card-actions" wx:if="{{item.bothConfirmed}}">
|
||||
<button class="btn-chat" bindtap="openChat" data-partnerid="{{item.partnerId}}">开启聊天</button>
|
||||
<text class="service-hint">双方已确认,可查看公益活动和服务站</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
@@ -55,10 +55,7 @@ Page({
|
||||
});
|
||||
if (res.statusCode === 200 && res.data.success) {
|
||||
this.setData({ myConfirmed: true, bothConfirmed: res.data.data.bothConfirmed });
|
||||
if (res.data.data.bothConfirmed) {
|
||||
wx.showToast({ title: '双方已确认!', icon: 'success' });
|
||||
setTimeout(() => this.openChat(), 1500);
|
||||
}
|
||||
if (res.data.data.bothConfirmed) wx.showToast({ title: '双方已确认,可查看公益服务', icon: 'success' });
|
||||
}
|
||||
} catch (e) { wx.showToast({ title: '操作失败', icon: 'none' }); }
|
||||
},
|
||||
@@ -76,7 +73,4 @@ Page({
|
||||
} catch (e) { wx.showToast({ title: '操作失败', icon: 'none' }); }
|
||||
},
|
||||
|
||||
openChat() {
|
||||
wx.navigateTo({ url: `/pages/chat/chat?partnerId=${this.data.partnerId}` });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,21 +7,20 @@
|
||||
<view class="status-section" wx:if="{{bothConfirmed}}">
|
||||
<view class="success-icon">✅</view>
|
||||
<text class="success-title">双方已确认!</text>
|
||||
<text class="success-hint">你们可以开始聊天了</text>
|
||||
<button class="chat-btn" bindtap="openChat">开启聊天</button>
|
||||
<text class="success-hint">双方意向已确认,可查看公益活动和服务站</text>
|
||||
</view>
|
||||
|
||||
<view class="status-section" wx:elif="{{myConfirmed}}">
|
||||
<view class="waiting-icon">⏳</view>
|
||||
<text class="waiting-title">已发送邀请</text>
|
||||
<text class="waiting-hint">对方确认后你们可以开始聊天</text>
|
||||
<text class="waiting-hint">对方确认后将收到平台通知</text>
|
||||
<button class="decline-btn" bindtap="onDecline">暂不联系</button>
|
||||
</view>
|
||||
|
||||
<view class="status-section" wx:else>
|
||||
<view class="pending-icon">💬</view>
|
||||
<text class="pending-title">向对方表达兴趣</text>
|
||||
<text class="pending-hint">双方确认后才能开启线上沟通</text>
|
||||
<text class="pending-hint">双方确认后将收到平台通知,并可查看公益服务</text>
|
||||
<button class="confirm-btn" bindtap="onConfirm">确认开启沟通</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -305,10 +305,7 @@ Page({
|
||||
const result = await matchStore.like(userId);
|
||||
this.setData({ users: this.data.users.filter((u) => u.userId !== userId) });
|
||||
if (result?.mutualMatch) {
|
||||
appStore.toastSuccess('双向喜欢!可以开始聊天了');
|
||||
if (result.matchedConversationId) {
|
||||
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
|
||||
}
|
||||
appStore.toastSuccess('双向意向匹配成功,可查看公益服务');
|
||||
} else {
|
||||
appStore.toastSuccess('已表达喜欢');
|
||||
}
|
||||
|
||||
@@ -141,12 +141,7 @@ Page({
|
||||
wx.navigateTo({ url: to.detail(userId) });
|
||||
},
|
||||
|
||||
/** 互相喜欢 → 开始聊天(已 Mutual,符合第 3 条) */
|
||||
onStartChat(e) {
|
||||
const conversationId = e.currentTarget.dataset.conversationId;
|
||||
if (!conversationId) return;
|
||||
wx.navigateTo({ url: to.chat(conversationId) });
|
||||
},
|
||||
/** 互相喜欢 → 查看资料和公益服务 */
|
||||
|
||||
/**
|
||||
* 喜欢我的人 → 喜欢 TA(like-back)
|
||||
@@ -163,12 +158,9 @@ Page({
|
||||
likersList: matchStore.state.likersList.filter((u) => u.userId !== userId),
|
||||
});
|
||||
if (result && result.mutualMatch) {
|
||||
appStore.toastSuccess('双向喜欢!可以开始聊天了');
|
||||
appStore.toastSuccess('双向意向匹配成功,可查看公益服务');
|
||||
// 刷新互相喜欢列表
|
||||
try { await matchStore.fetchMutualList({ pageSize: PAGE_SIZE }); } catch (e2) { /* ignore */ }
|
||||
if (result.matchedConversationId) {
|
||||
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
|
||||
}
|
||||
} else {
|
||||
appStore.toastSuccess('已表达喜欢');
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<view class="item" wx:for="{{mutualList}}" wx:key="conversationId">
|
||||
<ui-user-card user="{{item}}" bind:tap="onViewDetail">
|
||||
<view slot="actions">
|
||||
<button class="btn-primary" catchtap="onStartChat" data-conversation-id="{{item.conversationId}}">开始聊天</button>
|
||||
<button class="btn-primary" catchtap="onViewDetail" data-user-id="{{item.userId}}">查看资料</button>
|
||||
</view>
|
||||
</ui-user-card>
|
||||
</view>
|
||||
@@ -45,7 +45,7 @@
|
||||
wx:else
|
||||
illustration="heart"
|
||||
title="还没有互相喜欢的人"
|
||||
description="去「找对象」遇见合适的人,互相喜欢就能聊天啦"
|
||||
description="去「找对象」遇见合适的人,双方匹配后可查看公益服务"
|
||||
actions="{{[{text:'去「找对象', event:'discover', variant:'primary'}]}}"
|
||||
bind:action="onEmptyAction"
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
const { authStore, appStore, matchStore } = require('../../stores')
|
||||
const api = require('../../utils/api')
|
||||
// 顶部安全区:实测右上角胶囊按钮矩形,避免品牌栏「消息」入口被胶囊压住(见 utils/nav-metrics.js)
|
||||
// 顶部安全区:实测右上角胶囊按钮矩形,避免品牌栏入口被胶囊压住(见 utils/nav-metrics.js)
|
||||
const { navSafeData } = require('../../utils/nav-metrics')
|
||||
|
||||
// 4×2 宫格的视觉映射(tone → index.wxss 的 .tone-* 渐变砖;subtitle → 主标题下的说明行)。
|
||||
@@ -244,10 +244,7 @@ Page({
|
||||
const result = await matchStore.like(userId)
|
||||
this.setData({ recommendations: this.data.recommendations.filter((item) => item.userId !== userId) })
|
||||
if (result?.mutualMatch) {
|
||||
appStore.toastSuccess('双向喜欢!可以开始聊天了')
|
||||
if (result.matchedConversationId) {
|
||||
wx.navigateTo({ url: `/pages/chat/chat?id=${result.matchedConversationId}` })
|
||||
}
|
||||
appStore.toastSuccess('双向意向匹配成功,可查看公益服务')
|
||||
} else {
|
||||
appStore.toastSuccess('已表达喜欢')
|
||||
}
|
||||
@@ -327,7 +324,7 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/anti-fraud/anti-fraud' })
|
||||
},
|
||||
onArticleTap(e) { wx.navigateTo({ url: `/pages/content-detail/content-detail?id=${e.currentTarget.dataset.id}` }) },
|
||||
onMessages() { wx.switchTab({ url: '/pages/messages/messages' }) },
|
||||
onMessages() { wx.navigateTo({ url: '/pages/notifications/notifications' }) },
|
||||
|
||||
formatDate(timestamp) {
|
||||
if (!timestamp) return ''
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="brand-actions" bindtap="onMessages">
|
||||
<text class="message-label">消息</text>
|
||||
<text class="message-label">通知</text>
|
||||
<text wx:if="{{unreadCount}}" class="badge">{{unreadCount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -47,8 +47,8 @@ Page({
|
||||
try {
|
||||
const result = action === 'like' ? await matchStore.like(userId) : await matchStore.pass(userId);
|
||||
this.setData({ recommendations: this.data.recommendations.filter((item) => item.userId !== userId) });
|
||||
if (action === 'like' && result && result.mutualMatch && result.matchedConversationId) {
|
||||
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
|
||||
if (action === 'like' && result && result.mutualMatch) {
|
||||
appStore.toastSuccess('双向意向匹配成功,可查看公益服务');
|
||||
} else {
|
||||
appStore.toastSuccess(action === 'like' ? '已表达喜欢' : '已暂不考虑');
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ Page({
|
||||
|
||||
// ============ 匹配 ============
|
||||
|
||||
onGoMessages() { wx.switchTab({ url: ROUTE.TAB.MESSAGES }); },
|
||||
onGoMessages() { wx.navigateTo({ url: ROUTE.SETTINGS.NOTIFICATIONS }); },
|
||||
onGoLikers() { wx.navigateTo({ url: ROUTE.DETAIL.LIKERS }); },
|
||||
|
||||
// ============ 账户与安全 ============
|
||||
|
||||
@@ -163,10 +163,7 @@ Page({
|
||||
relationStatus: RELATION_STATUS.MUTUAL_MATCH,
|
||||
actionLoading: false,
|
||||
});
|
||||
appStore.toastSuccess('双向喜欢!可以开始聊天了');
|
||||
if (result.matchedConversationId) {
|
||||
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
|
||||
}
|
||||
appStore.toastSuccess('双向意向匹配成功,可查看公益服务');
|
||||
} else {
|
||||
this.setData({
|
||||
isLiked: true,
|
||||
@@ -185,19 +182,6 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 开始聊天(仅 Mutual 后可见,AGENTS 第 3 条)
|
||||
*/
|
||||
onGoChat() {
|
||||
const { userId, canChat, profile } = this.data;
|
||||
if (!canChat) {
|
||||
appStore.toast('双方互相喜欢后才能开始聊天');
|
||||
return;
|
||||
}
|
||||
// matchedConversationId 优先来自最近 like 结果;无则用 userId 占位由 chat 页拉取
|
||||
const conversationId = (profile && profile.matchedConversationId) || userId;
|
||||
wx.navigateTo({ url: to.chat(conversationId) });
|
||||
},
|
||||
|
||||
/**
|
||||
* 举报:跳转 report 页,不在本页处理(AGENTS 第 7 条:举报内容不在此暴露)
|
||||
|
||||
@@ -113,14 +113,6 @@
|
||||
bind:tap="onReport"
|
||||
disabled="{{actionLoading}}"
|
||||
>举报</ui-button>
|
||||
<ui-button
|
||||
wx:if="{{canChat}}"
|
||||
variant="secondary"
|
||||
size="md"
|
||||
custom-class="profile-actions__btn"
|
||||
bind:tap="onGoChat"
|
||||
disabled="{{actionLoading}}"
|
||||
>开始聊天</ui-button>
|
||||
<ui-button
|
||||
wx:if="{{!isLiked}}"
|
||||
variant="primary"
|
||||
|
||||
@@ -14,6 +14,7 @@ const profileService = require('./profileService');
|
||||
const verificationService = require('./verificationService');
|
||||
const recommendationService = require('./recommendationService');
|
||||
const matchService = require('./matchService');
|
||||
// 兼容未注册的历史聊天页面源码;用户端不再暴露该服务。
|
||||
const messageService = require('./messageService');
|
||||
const activityService = require('./activityService');
|
||||
const matchmakerService = require('./matchmakerService');
|
||||
|
||||
@@ -20,8 +20,7 @@ const api = require('../utils/api');
|
||||
* 表达喜欢
|
||||
* POST /likes { targetUserId }
|
||||
*
|
||||
* 响应对齐 types.LikeResult:{ relationStatus, mutualMatch, matchedConversationId }。
|
||||
* mutualMatch=true 且 matchedConversationId 非空时,调用方可直接跳转聊天页。
|
||||
* 响应对齐 types.LikeResult:{ relationStatus, mutualMatch }。
|
||||
* @param {string} userId
|
||||
* @returns {Promise<import('../types').LikeResult>}
|
||||
*/
|
||||
|
||||
+5
-19
@@ -15,6 +15,7 @@
|
||||
const authStore = require('./authStore');
|
||||
const userStore = require('./userStore');
|
||||
const matchStore = require('./matchStore');
|
||||
// 兼容未注册的历史聊天页面源码;页面与服务端入口均已下线,不在启动流程中初始化。
|
||||
const messageStore = require('./messageStore');
|
||||
const activityStore = require('./activityStore');
|
||||
const matchmakerStore = require('./matchmakerStore');
|
||||
@@ -128,53 +129,40 @@ function isProfileApproved() {
|
||||
// ============ 跨 Store 编排 ============
|
||||
|
||||
/**
|
||||
* 拉黑用户:触发 MatchStore 与 MessageStore 同步副作用
|
||||
* - MatchStore:从推荐/Mutual 列表移除该用户
|
||||
* - MessageStore:标记对应会话不可发消息(AGENTS 第 4 条)
|
||||
* 拉黑用户:从推荐/Mutual 列表移除该用户。
|
||||
* @param {string} userId
|
||||
* @param {string} [reason]
|
||||
*/
|
||||
async function blockUser(userId, reason) {
|
||||
await matchStore.block(userId, reason);
|
||||
// 查找对应会话并标记停用
|
||||
const conv = messageStore.state.conversations.find(
|
||||
(c) => c.targetUserId === userId || c.conversationId === userId,
|
||||
);
|
||||
if (conv) messageStore.markConversationBlocked(conv.conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like 用户:若返回 mutualMatch=true,自动刷新会话列表
|
||||
* Like 用户:若返回 mutualMatch=true,仅保留匹配结果和公益服务引导。
|
||||
* @param {string} userId
|
||||
* @returns {Promise<import('../types').LikeResult>}
|
||||
*/
|
||||
async function likeUser(userId) {
|
||||
const result = await matchStore.like(userId);
|
||||
if (result && result.mutualMatch) {
|
||||
// Mutual Match 达成,刷新会话列表,让用户能立即开始聊天
|
||||
await messageStore.fetchConversations();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除匹配:触发 MatchStore 与 MessageStore 同步副作用
|
||||
* 解除匹配。
|
||||
* @param {string} matchId
|
||||
*/
|
||||
async function unmatchConversation(matchId) {
|
||||
await matchStore.unmatch(matchId);
|
||||
messageStore.markConversationBlocked(matchId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功牵手:双确认第二步(仅被请求方调用),成功后刷新 userStore 与会话列表
|
||||
* 成功牵手:双确认第二步(仅被请求方调用),成功后刷新 userStore。
|
||||
* @param {string} matchId
|
||||
*/
|
||||
async function confirmSuccess(matchId) {
|
||||
const result = await matchStore.confirmSuccess(matchId);
|
||||
// 成功牵手后关系状态变化,需刷新 userState
|
||||
try { await userStore.fetchStatus(); } catch (e) { /* 不阻塞主流程 */ }
|
||||
try { await messageStore.fetchConversations(); } catch (e) { /* 同上 */ }
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -185,14 +173,12 @@ async function confirmSuccess(matchId) {
|
||||
* 1. appStore.init() — 拉取服务端配置(不阻塞核心)
|
||||
* 2. authStore.hydrate() — 从本地恢复登录态
|
||||
* 3. userStore.hydrate() — 从本地恢复用户状态快照
|
||||
* 4. messageStore.hydrate() — 从本地恢复未读数(兜底)
|
||||
* 5. 若已登录,异步刷新 userStore.fetchStatus()(不阻塞启动)
|
||||
*/
|
||||
async function bootstrap() {
|
||||
// 同步阶段:本地状态恢复(避免白屏)
|
||||
authStore.hydrate();
|
||||
userStore.hydrate();
|
||||
messageStore.hydrate();
|
||||
|
||||
// 异步阶段:服务端配置 + 用户状态刷新
|
||||
// 配置失败不阻塞启动;状态失败由路由守卫处理(跳登录页)
|
||||
|
||||
@@ -163,7 +163,7 @@ class MatchStore extends BaseStore {
|
||||
/**
|
||||
* 拉黑用户(Block 优先级最高,AGENTS 第 4 条)
|
||||
* 拉黑后:本地从推荐/Mutual 列表移除;调用方应跳离当前用户详情页。
|
||||
* 跨 Store 副作用由 stores/index.js 编排:通知 messageStore 标记会话不可发消息。
|
||||
* 匹配结果由 stores/index.js 编排,不创建用户间聊天会话。
|
||||
*/
|
||||
async block(userId, reason) {
|
||||
if (this._actionLocks.has(`block:${userId}`)) return;
|
||||
|
||||
Vendored
-1
@@ -109,7 +109,6 @@ export interface Recommendation extends User {
|
||||
export interface LikeResult {
|
||||
relationStatus: RelationStatus;
|
||||
mutualMatch: boolean;
|
||||
matchedConversationId?: string;
|
||||
}
|
||||
|
||||
// ============ Conversation / Message(规范 §35-38, §61-63) ============
|
||||
|
||||
@@ -239,7 +239,6 @@ export interface LikeResult {
|
||||
mutual: boolean
|
||||
mutualMatch?: boolean
|
||||
matchCreated?: boolean
|
||||
matchedConversationId?: string
|
||||
}
|
||||
|
||||
// ===== 通知 =====
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
def test_miniapp_has_no_user_chat_pages_or_message_tab():
|
||||
app = json.loads((ROOT / "miniapp" / "app.json").read_text(encoding="utf-8"))
|
||||
assert not any(page in {"pages/messages/messages", "pages/chat/chat"} for page in app["pages"])
|
||||
assert not any(item.get("pagePath") == "pages/messages/messages" for item in app["tabBar"]["list"])
|
||||
|
||||
|
||||
def test_match_success_does_not_create_or_return_conversation():
|
||||
like = (ROOT / "backend/src/main/java/com/danyang/match/platform/match/controller/LikeController.java").read_text(encoding="utf-8")
|
||||
relationship = (ROOT / "backend/src/main/java/com/danyang/match/platform/relationship/service/RelationshipService.java").read_text(encoding="utf-8")
|
||||
assert "matchedConversationId" not in like
|
||||
assert "getOrCreateConversation" not in like
|
||||
assert "conversationAutoCreated" not in relationship
|
||||
|
||||
|
||||
def test_chat_transport_is_disabled_at_backend_boundary():
|
||||
guard = ROOT / "backend/src/main/java/com/danyang/match/platform/chat/config/ChatDisabledFilter.java"
|
||||
assert guard.exists()
|
||||
source = guard.read_text(encoding="utf-8")
|
||||
assert "/conversations" in source
|
||||
assert "/ws/chat" in source
|
||||
assert "CHAT_DISABLED" in source
|
||||
Reference in New Issue
Block a user