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。
This commit is contained in:
WorkBuddy
2026-09-18 16:37:33 +08:00
parent 69cf6a2cc7
commit 8dee67040c
12 changed files with 116 additions and 49 deletions
+10 -15
View File
@@ -36,7 +36,7 @@ function getActivityDetail(activityId) {
* @returns {Promise<{ registrationStatus: 'pending', registrationId: string }>}
*/
function register(activityId, payload = {}) {
const idempotencyKey = `activity-register:${activityId}:${Date.now()}`;
const idempotencyKey = api.idempotencyKey('activity-register', activityId);
return api.post(`/activities/${encodeURIComponent(activityId)}/register`, payload, undefined, idempotencyKey);
}
@@ -66,21 +66,17 @@ function getMyActivities(params = {}) {
* @param {{ rating: number, content: string }} payload
* @returns {Promise<{ submitted: true }>}
*/
function submitFeedback(activityId, payload) {
const idempotencyKey = `activity-feedback:${activityId}:${Date.now()}`;
return api.post(`/activities/${encodeURIComponent(activityId)}/feedback`, payload, undefined, idempotencyKey);
}
/**
* 提交活动互选意向(API_INVENTORY 行 98,AGENTS 第 6 条:单向意向保密)
* @param {string} activityId
* @param {{ likedUserIds: string[] }} payload
* @returns {Promise<{ submitted: true, matchedUserIds: string[] }>}
* matchedUserIds 仅包含双向匹配成功的人;未匹配的人不会泄露是否被对方选择。
* 提交活动反馈(含表达意向 via interestTargetUserIds)
*
* 注意:此前的 `submitInterests()` → `POST /activities/{id}/interests`
* **后端不存在该端点**(Phase 16 已把表达意向合并进本流程),调用必然 404。
* 全仓确认无页面调用方,属设计残留,已删除。若需要独立的互选提交接口,
* 须先由产品定义语义并补后端端点。
*/
function submitInterests(activityId, payload) {
const idempotencyKey = `activity-interests:${activityId}:${Date.now()}`;
return api.post(`/activities/${encodeURIComponent(activityId)}/interests`, payload, undefined, idempotencyKey);
function submitFeedback(activityId, payload) {
const idempotencyKey = api.idempotencyKey('activity-feedback', activityId);
return api.post(`/activities/${encodeURIComponent(activityId)}/feedback`, payload, undefined, idempotencyKey);
}
module.exports = {
@@ -90,5 +86,4 @@ module.exports = {
cancelRegistration,
getMyActivities,
submitFeedback,
submitInterests,
};
+1 -1
View File
@@ -18,7 +18,7 @@ const api = require('../utils/api');
function submitAppeal(payload) {
// 幂等键:申诉类型 + 关联处置 ID + 当前秒,防止用户多次点击重复提交
const actionKey = payload.relatedActionId || 'no_action';
const idempotencyKey = `appeal:${payload.appealType}:${actionKey}:${Math.floor(Date.now() / 1000)}`;
const idempotencyKey = api.idempotencyKey('appeal', `${payload.appealType}:${actionKey}`);
return api.post('/appeals', payload, undefined, idempotencyKey);
}
+2 -2
View File
@@ -36,11 +36,11 @@ function refresh() {
/**
* 登出(API_INVENTORY 行 11)
* 幂等:Idempotency-Key = "logout:" + 当前毫秒时间,便于后端去重。
* 幂等:键由 api 层托管,网络异常重试时复用同一把,避免后端重复记账。
* @returns {Promise<void>}
*/
function logout() {
return api.post('/auth/logout', {}, undefined, 'logout:' + Date.now());
return api.post('/auth/logout', {}, undefined, api.idempotencyKey('logout'));
}
module.exports = {
+7 -7
View File
@@ -26,7 +26,7 @@ const api = require('../utils/api');
* @returns {Promise<import('../types').LikeResult>}
*/
function like(userId) {
const idempotencyKey = `like:${userId}:${Date.now()}`;
const idempotencyKey = api.idempotencyKey('like', userId);
return api.post('/likes', { targetUserId: userId }, undefined, idempotencyKey);
}
@@ -51,7 +51,7 @@ function unlike(userId) {
* @returns {Promise<void>}
*/
function pass(userId) {
const idempotencyKey = `pass:${userId}:${Date.now()}`;
const idempotencyKey = api.idempotencyKey('pass', userId);
return api.post('/passes', { targetUserId: userId }, undefined, idempotencyKey);
}
@@ -65,7 +65,7 @@ function pass(userId) {
* @returns {Promise<void>}
*/
function block(userId, payload = {}) {
const idempotencyKey = `block:${userId}:${Date.now()}`;
const idempotencyKey = api.idempotencyKey('block', userId);
return api.post('/blocks', { targetUserId: userId, reason: payload.reason }, undefined, idempotencyKey);
}
@@ -145,7 +145,7 @@ function getMatches(params = {}) {
* @returns {Promise<void>}
*/
function unmatch(matchId) {
const idempotencyKey = `unmatch:${matchId}:${Date.now()}`;
const idempotencyKey = api.idempotencyKey('unmatch', matchId);
return api.post(`/matches/${encodeURIComponent(matchId)}/unmatch`, {}, undefined, idempotencyKey);
}
@@ -158,7 +158,7 @@ function unmatch(matchId) {
* @returns {Promise<{ requestId: string, initiatedBy: string, status: 'pending' }>}
*/
function requestSuccess(matchId) {
const idempotencyKey = `success-req:${matchId}:${Date.now()}`;
const idempotencyKey = api.idempotencyKey('success-req', matchId);
return api.post(`/matches/${encodeURIComponent(matchId)}/success/request`, {}, undefined, idempotencyKey);
}
@@ -171,7 +171,7 @@ function requestSuccess(matchId) {
* @returns {Promise<{ relationshipStatus: string }>}
*/
function confirmSuccess(matchId) {
const idempotencyKey = `success-confirm:${matchId}:${Date.now()}`;
const idempotencyKey = api.idempotencyKey('success-confirm', matchId);
return api.post(`/matches/${encodeURIComponent(matchId)}/success/confirm`, {}, undefined, idempotencyKey);
}
@@ -182,7 +182,7 @@ function confirmSuccess(matchId) {
* @returns {Promise<void>}
*/
function declineSuccess(matchId) {
const idempotencyKey = `success-decline:${matchId}:${Date.now()}`;
const idempotencyKey = api.idempotencyKey('success-decline', matchId);
return api.post(`/matches/${encodeURIComponent(matchId)}/success/decline`, {}, undefined, idempotencyKey);
}
+1 -1
View File
@@ -15,7 +15,7 @@ const api = require('../utils/api');
* @returns {Promise<{ serviceId: string, status: 'submitted' }>}
*/
function applyService(payload) {
const idempotencyKey = `matchmaker-apply:${Date.now()}`;
const idempotencyKey = api.idempotencyKey('matchmaker-apply');
return api.post('/matchmaker/services', payload, undefined, idempotencyKey);
}
+8 -8
View File
@@ -22,7 +22,7 @@ function getProfile() {
*/
function updateProfile(payload) {
// PUT 默认幂等:用 userId + 字段指纹作为幂等键,避免重复提交时脏写
const idempotencyKey = 'profile-put:' + (payload && payload.userId || 'me') + ':' + Date.now();
const idempotencyKey = api.idempotencyKey('profile-put', (payload && payload.userId) || 'me');
return api.put('/me/profile', payload, undefined, idempotencyKey);
}
@@ -40,7 +40,7 @@ function getPreferences() {
* @returns {Promise<import('../types').PartnerPreference>}
*/
function updatePreferences(payload) {
const idempotencyKey = 'preferences-put:' + Date.now();
const idempotencyKey = api.idempotencyKey('preferences-put');
return api.put('/me/preferences', payload, undefined, idempotencyKey);
}
@@ -67,7 +67,7 @@ function deletePhoto(photoId) {
* @returns {Promise<{ avatarUrl: string }>}
*/
function setMainPhoto(photoId) {
const idempotencyKey = 'photo-main:' + photoId + ':' + Date.now();
const idempotencyKey = api.idempotencyKey('photo-main', photoId);
return api.put(`/me/photos/${photoId}/main`, {}, undefined, idempotencyKey);
}
@@ -94,7 +94,7 @@ function getPrivacySettings() {
* @returns {Promise<import('../types').PrivacySettings>}
*/
function updatePrivacySettings(payload) {
const idempotencyKey = 'privacy-put:' + Date.now();
const idempotencyKey = api.idempotencyKey('privacy-put');
return api.put('/me/privacy-settings', payload, undefined, idempotencyKey);
}
@@ -129,7 +129,7 @@ function getReviewStatus() {
* @returns {Promise<{ reviewStatus: 'pending', submittedAt: number }>}
*/
function submitReview() {
const idempotencyKey = 'review-submit:' + Date.now();
const idempotencyKey = api.idempotencyKey('review-submit');
return api.post('/me/profile/submit-review', {}, undefined, idempotencyKey);
}
@@ -147,7 +147,7 @@ function getRelationshipStatus() {
* @returns {Promise<{ status: 'paused', recommendationEnabled: boolean }>}
*/
function pauseSearching() {
const idempotencyKey = 'rel-pause:' + Date.now();
const idempotencyKey = api.idempotencyKey('rel-pause');
return api.patch('/me/relationship-status', { status: 'paused' }, undefined, idempotencyKey);
}
@@ -156,7 +156,7 @@ function pauseSearching() {
* @returns {Promise<{ status: 'searching', recommendationEnabled: boolean }>}
*/
function resumeSearching() {
const idempotencyKey = 'rel-resume:' + Date.now();
const idempotencyKey = api.idempotencyKey('rel-resume');
return api.patch('/me/relationship-status', { status: 'searching' }, undefined, idempotencyKey);
}
@@ -165,7 +165,7 @@ function resumeSearching() {
* @returns {Promise<{ status: 'getting_to_know', recommendationEnabled: boolean }>}
*/
function markGettingToKnow() {
const idempotencyKey = 'rel-gtk:' + Date.now();
const idempotencyKey = api.idempotencyKey('rel-gtk');
return api.patch('/me/relationship-status', { status: 'getting_to_know' }, undefined, idempotencyKey);
}
+1 -1
View File
@@ -21,7 +21,7 @@ const api = require('../utils/api');
*/
function submitReport(payload) {
// 幂等键:目标用户 + 举报类型 + 当前秒,防止用户多次点击重复提交
const idempotencyKey = `report:${payload.targetUserId}:${payload.reportType}:${Math.floor(Date.now() / 1000)}`;
const idempotencyKey = api.idempotencyKey('report', `${payload.targetUserId}:${payload.reportType}`);
return api.post('/reports', payload, undefined, idempotencyKey);
}
+1 -1
View File
@@ -15,7 +15,7 @@ const api = require('../utils/api');
function submit(payload) {
// 幂等键:身份证号后4位 + 当前秒,避免用户多次点击
const tail = (payload.idCardNumber || '').slice(-4).toUpperCase();
const idempotencyKey = 'verification-submit:' + tail + ':' + Math.floor(Date.now() / 1000);
const idempotencyKey = api.idempotencyKey('verification-submit', tail);
return api.post('/verification/submit', payload, undefined, idempotencyKey);
}