Files
danyang-match/miniapp/services/matchService.js
WorkBuddy 8dee67040c fix(miniapp): 幂等键改为按操作稳定;删除指向 404 的互选死代码
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。
2026-09-18 16:37:33 +08:00

205 lines
7.0 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.
/**
* 关系与匹配服务
*
* 封装: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,
};