1) 幂等键(配合后端机制)
原写法把 Date.now() 拼进幂等键,每次调用都生成新键,即使后端实现了幂等
也永远不会命中 —— 声明端与实现端各自都坏。
api.idempotencyKey(scope, subject):键缓存在 _pendingIdempotency(TTL 10min),
同一逻辑操作在成功之前复用同一把键,收到 200+success 才释放;失败(含网络错误)
不释放,下次重试沿用同一把键。25 处调用点(8 个 service + 2 个 TS 页面)全部替换,
Date.now 幂等键写法残留经 grep 为 0。
2) 删除死代码 submitInterests
它请求 POST /activities/{id}/interests —— 后端从未实现该端点
(Phase 16 已把表达意向合并进 /activities/{id}/feedback 的
interestTargetUserIds)。全仓确认无任何页面/组件调用方。
两个文件各留注释说明删除原因与恢复条件:若产品确认需要独立的
「提交互选」端点,应先定义语义并补后端实现,再恢复前端方法。
store 头部关于 AGENTS 第 6 条(单向意向保密)的注释同步改为引用
submitFeedback 返回的 newMutuals。
205 lines
7.0 KiB
JavaScript
205 lines
7.0 KiB
JavaScript
/**
|
||
* 关系与匹配服务
|
||
*
|
||
* 封装: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, matchedConversationId }。
|
||
* mutualMatch=true 且 matchedConversationId 非空时,调用方可直接跳转聊天页。
|
||
* @param {string} userId
|
||
* @returns {Promise<import('../types').LikeResult>}
|
||
*/
|
||
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<void>}
|
||
*/
|
||
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<void>}
|
||
*/
|
||
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<void>}
|
||
*/
|
||
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<void>}
|
||
*/
|
||
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<import('../types').User>, 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<import('../types').User & { relationTime?: number }>, 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<import('../types').FateItem>, 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<void>}
|
||
*/
|
||
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<void>}
|
||
*/
|
||
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,
|
||
};
|