/** * 关系与匹配服务 * * 封装:Like/Pass/Block/取消/解除 + 缘分三榜 + 黑名单 + Unmatch + 成功牵手双确认。 * * 路径基线:2026-09-14 的 match 模块(LikeController/PassController/BlockController/MatchController) * 为 canonical 路径 —— /likes、/passes、/blocks、/matches;缘分三榜保留 /relations/fate/*。 * 旧的 /relations/{id}/like|pass|block、/relations/mutual|liked|likers、/me/blacklist 已废止。 * * 关键不变量(AGENTS.md): * - 第 4 条:Block 即时禁止互动——前端调用 block 后必须立即更新本地会话与列表。 * - 第 5 条:红娘不可替用户 Like——本层仅供当前用户调用,不接受 matchmakerId 参数。 * - 第 6 条:成功牵手必须双确认——success/request 与 success/confirm 分两次调用。 * 单向意向不泄露:A Like B 但未 Mutual,本层不提供"谁喜欢我"的可识别列表, * 仅返回计数 / 是否被 liked 的布尔,详情走 /relations/fate/likers(仅返回脱敏字段)。 */ const api = require('../utils/api'); /** * 表达喜欢 * POST /likes { targetUserId } * * 响应对齐 types.LikeResult:{ relationStatus, mutualMatch }。 * @param {string} userId * @returns {Promise} */ function like(userId) { const idempotencyKey = api.idempotencyKey('like', userId); return api.post('/likes', { targetUserId: userId }, undefined, idempotencyKey); } /** * 取消喜欢 * DELETE /likes/{userId} * * 注意:Mutual 后不可直接取消(后端返回状态冲突,提示先解除匹配),前端不应阻断调用。 * @param {string} userId * @returns {Promise} */ function unlike(userId) { return api.delete(`/likes/${encodeURIComponent(userId)}`, {}, undefined, `unlike:${userId}:${Date.now()}`); } /** * 表示不合适 * POST /passes { targetUserId } * * Pass 是单向意向,不会通知对方(AGENTS 第 6 条)。 * @param {string} userId * @returns {Promise} */ function pass(userId) { const idempotencyKey = api.idempotencyKey('pass', userId); return api.post('/passes', { targetUserId: userId }, undefined, idempotencyKey); } /** * 拉黑用户(AGENTS 第 4 条:Block 优先级最高) * POST /blocks { targetUserId, reason? } * * 前端立即停止与该用户的所有互动,从推荐/搜索/会话列表移除。 * @param {string} userId * @param {{ reason?: string }} [payload] * @returns {Promise} */ function block(userId, payload = {}) { const idempotencyKey = api.idempotencyKey('block', userId); return api.post('/blocks', { targetUserId: userId, reason: payload.reason }, undefined, idempotencyKey); } /** * 解除拉黑 * DELETE /blocks/{userId} * @param {string} userId * @returns {Promise} */ function unblock(userId) { return api.delete(`/blocks/${encodeURIComponent(userId)}`, {}, undefined, `unblock:${userId}:${Date.now()}`); } /** * 获取互相喜欢列表(Mutual Matches) * GET /relations/fate/mutual * @param {{ cursor?: string, pageSize?: number }} params * @returns {Promise<{ items: Array, nextCursor: string|null }>} */ function getMutualList(params = {}) { return api.get('/relations/fate/mutual', params); } /** * 获取我喜欢的人列表 * GET /relations/fate/liked * @param {{ cursor?: string, pageSize?: number }} params * @returns {Promise<{ items: Array, nextCursor: string|null }>} */ function getLikedByMe(params = {}) { return api.get('/relations/fate/liked', params); } /** * 获取喜欢我的人列表 * GET /relations/fate/likers * * 仅返回脱敏字段(昵称/年龄/地区/是否已 Mutual),不泄露对方明示身份。 * @param {{ cursor?: string, pageSize?: number }} params * @returns {Promise<{ items: Array, nextCursor: string|null }>} */ function getLikers(params = {}) { return api.get('/relations/fate/likers', params); } /** * 获取黑名单列表(黑名单管理页) * GET /blocks * * 拉黑/解除拉黑走 blocks 的 POST/DELETE,本接口仅查询。 * 返回字段已脱敏(不返回对方手机号/身份证等敏感信息)。 * @param {{ cursor?: string, pageSize?: number }} params * @returns {Promise<{ items: Array<{ userId: string, userName: string, avatarUrl: string|null, blockedAt: number|null, reason: string|null }>, nextCursor: string|null }>} */ function getBlockedList(params = {}) { return api.get('/blocks', params); } /** * 获取我的匹配列表 * GET /matches * * 与缘分 mutual 区别:matches 含 Match 状态、episode、服务跟进字段。 * @param {{ cursor?: string, pageSize?: number }} params * @returns {Promise<{ items: Array<{ id: string, source: string, sourceId: string|null, status: string, peerUserId: string, matchedAt: number|null, lastActivityAt: number|null, episode: number }>, nextCursor: string|null }>} */ function getMatches(params = {}) { return api.get('/matches', params); } /** * 解除匹配 * POST /matches/{matchId}/unmatch * * 解除后双方会话立即停用,影响范围仅次于 Block。 * @param {string} matchId * @returns {Promise} */ function unmatch(matchId) { const idempotencyKey = api.idempotencyKey('unmatch', matchId); return api.post(`/matches/${encodeURIComponent(matchId)}/unmatch`, {}, undefined, idempotencyKey); } /** * 发起成功牵手请求(AGENTS 第 6 条:成功牵手必须双确认) * POST /matches/{matchId}/success/request * * 仅发起方调用一次,需对方在有效期内 confirm。 * @param {string} matchId * @returns {Promise<{ requestId: string, initiatedBy: string, status: 'pending' }>} */ function requestSuccess(matchId) { const idempotencyKey = api.idempotencyKey('success-req', matchId); return api.post(`/matches/${encodeURIComponent(matchId)}/success/request`, {}, undefined, idempotencyKey); } /** * 确认成功牵手 * POST /matches/{matchId}/success/confirm * * 仅被请求方调用,双方都确认后关系置为 matched_successfully。 * @param {string} matchId * @returns {Promise<{ relationshipStatus: string }>} */ function confirmSuccess(matchId) { const idempotencyKey = api.idempotencyKey('success-confirm', matchId); return api.post(`/matches/${encodeURIComponent(matchId)}/success/confirm`, {}, undefined, idempotencyKey); } /** * 拒绝成功牵手 * POST /matches/{matchId}/success/decline * @param {string} matchId * @returns {Promise} */ function declineSuccess(matchId) { const idempotencyKey = api.idempotencyKey('success-decline', matchId); return api.post(`/matches/${encodeURIComponent(matchId)}/success/decline`, {}, undefined, idempotencyKey); } module.exports = { like, unlike, pass, block, unblock, getMutualList, getLikedByMe, getLikers, getBlockedList, getMatches, unmatch, requestSuccess, confirmSuccess, declineSuccess, };