Files
WorkBuddy 8b505d9133
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
feat: disable user messaging and retain公益 service guidance
2026-10-15 13:38:17 +08:00

385 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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');
const { formatConversationTime } = require('../utils/datetime');
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<import('../types').LikeResult>}
* 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 列表移除;调用方应跳离当前用户详情页。
* 匹配结果由 stores/index.js 编排,不创建用户间聊天会话。
*/
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: this._decorateBlockedList(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, ...this._decorateBlockedList(result.items)],
blockedCursor: result.nextCursor || null,
blockedHasMore: !!result.nextCursor,
});
return result;
}
/**
* 黑名单列表装饰
*
* pages/blacklist 模板读取 `item.blockedAtText`,但该字段服务端不产出
* (后端只给 epoch 毫秒的 blockedAt),故进入 store 时统一格式化一次,
* 与 messageStore._decorateConversations 同构,避免每个页面各自实现。
*/
_decorateBlockedList(items) {
return (items || []).map((u) => ({
...u,
blockedAtText: formatConversationTime(u.blockedAt),
}));
}
/** 拉取喜欢我的人计数(不暴露具体身份) */
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;