/** * MatchStore(规范 §131) * * 职责: * - 维护今日推荐列表(游标分页 + 加载状态) * - 维护 Mutual Matches(互相喜欢)列表 * - 维护我喜欢 / 喜欢我 计数(计数仅作徽标提示,不暴露具体身份) * - 处理 Like/Pass/Block/Unmatch/成功牵手双确认 等动作 * * 不变量(AGENTS.md): * - 第 2 条:Required 不可自动放宽——推荐列表完全由后端按 PartnerPreference 过滤, * 前端不做任何"放宽条件再查"的逻辑。 * - 第 4 条:Block 即时禁止互动——block 成功后,本地立即从推荐/Mutual 列表移除该用户。 * - 第 5 条:红娘不可替用户 Like——本 Store 只暴露当前用户操作 Like/Pass 的接口。 * - 第 6 条:成功牵手必须双确认——requestSuccess 与 confirmSuccess 分两次调用, * Store 内部维护 successPendingMatchIds 表示谁正在等待对方确认。 * - 第 6 条(隐式):单向意向不泄露——A Like B 但 B 未 Mutual 时, * getLikers 不在 Store 中暴露 B 的具体身份(仅后端返回的脱敏字段)。 */ const BaseStore = require('./baseStore'); const { matchService, recommendationService } = require('../services'); const { RECOMMENDATION_STATE, RELATION_STATUS } = require('../constants'); class MatchStore extends BaseStore { constructor() { super({ /** @type {import('../types').Recommendation[]} */ recommendations: [], /** @type {RECOMMENDATION_STATE[keyof RECOMMENDATION_STATE]} */ recState: RECOMMENDATION_STATE.IDLE, recCursor: null, dailyRemaining: 0, /** @type {import('../types').Conversation[]} */ mutualList: [], mutualCursor: null, mutualHasMore: true, /** 我喜欢的人列表(缘分中心 Tab) */ likedList: [], likedCursor: null, likedHasMore: true, /** 喜欢我的人列表(缘分中心 Tab,仅脱敏字段) */ likersList: [], likersCursor: null, likersHasMore: true, /** 黑名单列表(脱敏) */ blockedList: [], blockedCursor: null, blockedHasMore: true, likersCount: 0, likedByMeCount: 0, /** 正在等待对方确认成功牵手的 matchId 集合 */ successPendingMatchIds: [], lastError: null, }); /** 进行中的 action 防重锁,key=userId/matchId */ this._actionLocks = new Set(); } /** 获取今日推荐(首次) */ async fetchRecommendations() { if (this.state.recState === RECOMMENDATION_STATE.LOADING) return; this.setState({ recState: RECOMMENDATION_STATE.LOADING, lastError: null }); try { const result = await recommendationService.getRecommendations({ pageSize: 10 }); const items = result.items || []; this.setState({ recommendations: items, recCursor: result.nextCursor || null, dailyRemaining: result.dailyRemaining ?? 0, recState: items.length === 0 ? RECOMMENDATION_STATE.EMPTY : RECOMMENDATION_STATE.LOADED, }); return result; } catch (err) { this.setState({ recState: RECOMMENDATION_STATE.ERROR, lastError: err }); throw err; } } /** 加载更多推荐 */ async loadMoreRecommendations() { if (this.state.recState === RECOMMENDATION_STATE.LOADING) return; if (!this.state.recCursor) { this.setState({ recState: RECOMMENDATION_STATE.FINISHED }); return; } this.setState({ recState: RECOMMENDATION_STATE.LOADING }); try { const result = await recommendationService.getRecommendations({ cursor: this.state.recCursor, pageSize: 10, }); const items = result.items || []; this.setState({ recommendations: [...this.state.recommendations, ...items], recCursor: result.nextCursor || null, dailyRemaining: result.dailyRemaining ?? this.state.dailyRemaining, recState: items.length === 0 ? RECOMMENDATION_STATE.FINISHED : RECOMMENDATION_STATE.LOADED, }); } catch (err) { this.setState({ recState: RECOMMENDATION_STATE.ERROR, lastError: err }); throw err; } } /** * Like 一个用户 * @param {string} userId * @returns {Promise} * mutualMatch=true 表示刚刚达成双向匹配,调用方应跳转会话页或弹提示 */ async like(userId) { if (this._actionLocks.has(`like:${userId}`)) return; this._actionLocks.add(`like:${userId}`); try { const result = await matchService.like(userId); // 从推荐列表移除(无论是否 Mutual,已经做出判断) this.setState({ recommendations: this.state.recommendations.filter((r) => r.userId !== userId), }); return result; } finally { this._actionLocks.delete(`like:${userId}`); } } /** * 取消喜欢 * 注意:Mutual 后通常不能直接取消,后端会返回 MATCH_REQUIRED */ async unlike(userId) { if (this._actionLocks.has(`unlike:${userId}`)) return; this._actionLocks.add(`unlike:${userId}`); try { await matchService.unlike(userId); // 推荐/匹配列表不变,后端 relationship 变 neutral } finally { this._actionLocks.delete(`unlike:${userId}`); } } /** Pass(单向意向,对方不收到通知) */ async pass(userId) { if (this._actionLocks.has(`pass:${userId}`)) return; this._actionLocks.add(`pass:${userId}`); try { await matchService.pass(userId); this.setState({ recommendations: this.state.recommendations.filter((r) => r.userId !== userId), }); } finally { this._actionLocks.delete(`pass:${userId}`); } } /** * 拉黑用户(Block 优先级最高,AGENTS 第 4 条) * 拉黑后:本地从推荐/Mutual 列表移除;调用方应跳离当前用户详情页。 * 跨 Store 副作用由 stores/index.js 编排:通知 messageStore 标记会话不可发消息。 */ async block(userId, reason) { if (this._actionLocks.has(`block:${userId}`)) return; this._actionLocks.add(`block:${userId}`); try { await matchService.block(userId, reason ? { reason } : {}); this.setState({ recommendations: this.state.recommendations.filter((r) => r.userId !== userId), mutualList: this.state.mutualList.filter( (c) => c.targetUserId !== userId && c.conversationId !== userId, ), }); return { relationStatus: RELATION_STATUS.BLOCKED }; } finally { this._actionLocks.delete(`block:${userId}`); } } /** 解除拉黑 */ async unblock(userId) { if (this._actionLocks.has(`unblock:${userId}`)) return; this._actionLocks.add(`unblock:${userId}`); try { return await matchService.unblock(userId); } finally { this._actionLocks.delete(`unblock:${userId}`); } } /** 拉取 Mutual Matches 列表 */ async fetchMutualList(params = {}) { const result = await matchService.getMutualList(params); this.setState({ mutualList: result.items || [], mutualCursor: result.nextCursor || null, mutualHasMore: !!result.nextCursor, }); return result; } /** 加载更多 Mutual Matches */ async loadMoreMutualList() { if (!this.state.mutualCursor || !this.state.mutualHasMore) return; const result = await matchService.getMutualList({ cursor: this.state.mutualCursor }); this.setState({ mutualList: [...this.state.mutualList, ...(result.items || [])], mutualCursor: result.nextCursor || null, mutualHasMore: !!result.nextCursor, }); return result; } /** * 拉取我喜欢的人列表(缘分中心 Tab) * AGENTS 第 6 条(隐式):单向意向不泄露——本列表为"我"视角,可查看自己喜欢的人。 */ async fetchLikedList(params = {}) { const result = await matchService.getLikedByMe(params); this.setState({ likedList: result.items || [], likedCursor: result.nextCursor || null, likedHasMore: !!result.nextCursor, }); return result; } /** 加载更多我喜欢的人 */ async loadMoreLikedList() { if (!this.state.likedCursor || !this.state.likedHasMore) return; const result = await matchService.getLikedByMe({ cursor: this.state.likedCursor }); this.setState({ likedList: [...this.state.likedList, ...(result.items || [])], likedCursor: result.nextCursor || null, likedHasMore: !!result.nextCursor, }); return result; } /** * 拉取喜欢我的人列表(缘分中心 Tab) * AGENTS 第 6 条:单向意向不泄露——后端仅返回脱敏字段(昵称/年龄/地区/是否已 Mutual), * Store 不做任何二次身份还原。 */ async fetchLikersList(params = {}) { const result = await matchService.getLikers(params); this.setState({ likersList: result.items || [], likersCursor: result.nextCursor || null, likersHasMore: !!result.nextCursor, likersCount: result.totalCount ?? this.state.likersCount, }); return result; } /** 加载更多喜欢我的人 */ async loadMoreLikersList() { if (!this.state.likersCursor || !this.state.likersHasMore) return; const result = await matchService.getLikers({ cursor: this.state.likersCursor }); this.setState({ likersList: [...this.state.likersList, ...(result.items || [])], likersCursor: result.nextCursor || null, likersHasMore: !!result.nextCursor, }); return result; } /** * 拉取黑名单列表(黑名单管理页) * 返回字段已脱敏,不含对方手机号/身份证等敏感信息。 */ async fetchBlockedList(params = {}) { const result = await matchService.getBlockedList(params); this.setState({ blockedList: result.items || [], blockedCursor: result.nextCursor || null, blockedHasMore: !!result.nextCursor, }); return result; } /** 加载更多黑名单 */ async loadMoreBlockedList() { if (!this.state.blockedCursor || !this.state.blockedHasMore) return; const result = await matchService.getBlockedList({ cursor: this.state.blockedCursor }); this.setState({ blockedList: [...this.state.blockedList, ...(result.items || [])], blockedCursor: result.nextCursor || null, blockedHasMore: !!result.nextCursor, }); return result; } /** 拉取喜欢我的人计数(不暴露具体身份) */ async fetchLikersCount() { const result = await matchService.getLikers({ pageSize: 1 }); this.setState({ likersCount: result.totalCount || 0 }); return result.totalCount || 0; } /** 拉取我喜欢的人计数 */ async fetchLikedByMeCount() { const result = await matchService.getLikedByMe({ pageSize: 1 }); this.setState({ likedByMeCount: (result.items || []).length }); return result; } /** * 解除匹配(AGENTS 第 4 条:解除后双方会话立即停用) * @param {string} matchId */ async unmatch(matchId) { if (this._actionLocks.has(`unmatch:${matchId}`)) return; this._actionLocks.add(`unmatch:${matchId}`); try { await matchService.unmatch(matchId); this.setState({ mutualList: this.state.mutualList.filter((c) => c.conversationId !== matchId), }); } finally { this._actionLocks.delete(`unmatch:${matchId}`); } } /** * 发起成功牵手请求(AGENTS 第 6 条:双确认第一步) * @param {string} matchId * @returns {Promise<{ successRequestId: string, expiresAt: number, status: string }>} */ async requestSuccess(matchId) { if (this._actionLocks.has(`success-req:${matchId}`)) return; this._actionLocks.add(`success-req:${matchId}`); try { const result = await matchService.requestSuccess(matchId); this.setState({ successPendingMatchIds: [...new Set([...this.state.successPendingMatchIds, matchId])], }); return result; } finally { this._actionLocks.delete(`success-req:${matchId}`); } } /** * 确认成功牵手(双确认第二步,仅被请求方调用) * @param {string} matchId */ async confirmSuccess(matchId) { if (this._actionLocks.has(`success-confirm:${matchId}`)) return; this._actionLocks.add(`success-confirm:${matchId}`); try { const result = await matchService.confirmSuccess(matchId); // 成功后从 pending 列表移除 this.setState({ successPendingMatchIds: this.state.successPendingMatchIds.filter((id) => id !== matchId), }); return result; } finally { this._actionLocks.delete(`success-confirm:${matchId}`); } } } const matchStore = new MatchStore(); module.exports = matchStore;