Files
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

186 lines
6.2 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.
/**
* ActivityStore(规范 §131)
*
* 职责:
* - 维护活动列表(首页 + 我的活动)
* - 维护活动报名状态 / 互选意向(AGENTS 第 6 条:单向意向保密)
* - 维护我报名的活动集合,用于徽标与"我的活动"页
*
* 不变量(AGENTS.md):
* - 第 1 条:用户永久免费——活动 feeType=paid 字段仅作展示,前端不实现任何支付入口。
* - 第 6 条:活动单向意向保密——表达意向走 submitFeedback 的 interestTargetUserIds,
* 其返回的 newMutuals 仅含双向匹配成功的人;未匹配的人不在前端可见;
* Store 不缓存"谁单独选了我"的字段。
*/
const BaseStore = require('./baseStore');
const { activityService } = require('../services');
const { ACTIVITY_STATUS, REGISTRATION_STATUS } = require('../constants');
class ActivityStore extends BaseStore {
constructor() {
super({
/** @type {import('../types').Activity[]} */
activities: [],
activitiesCursor: null,
activitiesHasMore: true,
activitiesLoading: false,
/** @type {import('../types').Activity[]} */
myActivities: [],
myActivitiesCursor: null,
myActivitiesHasMore: true,
myActivitiesLoading: false,
/** @type {Record<string, import('../types').Activity>} 活动详情缓存 */
detailCache: {},
/** @type {Record<string, import('../types').RegistrationStatus>} activityId -> 报名状态 */
registrationStatusByActivity: {},
/** 我报名成功且未结束的活动数(徽标用) */
myActiveCount: 0,
lastError: null,
});
}
/** 拉取活动列表(首页) */
async fetchActivities(params = {}) {
if (this.state.activitiesLoading) return;
this.setState({ activitiesLoading: true, lastError: null });
try {
const result = await activityService.getActivities({
status: ACTIVITY_STATUS.REGISTRATION_OPEN,
...params,
});
this.setState({
activities: result.items || [],
activitiesCursor: result.nextCursor || null,
activitiesHasMore: !!result.nextCursor,
activitiesLoading: false,
});
return result;
} catch (err) {
this.setState({ activitiesLoading: false, lastError: err });
throw err;
}
}
/** 加载更多活动 */
async loadMoreActivities() {
if (this.state.activitiesLoading || !this.state.activitiesCursor) return;
this.setState({ activitiesLoading: true });
try {
const result = await activityService.getActivities({
cursor: this.state.activitiesCursor,
pageSize: 20,
});
this.setState({
activities: [...this.state.activities, ...(result.items || [])],
activitiesCursor: result.nextCursor || null,
activitiesHasMore: !!result.nextCursor,
activitiesLoading: false,
});
} catch (err) {
this.setState({ activitiesLoading: false, lastError: err });
throw err;
}
}
/** 拉取我的活动 */
async fetchMyActivities(params = {}) {
if (this.state.myActivitiesLoading) return;
this.setState({ myActivitiesLoading: true, lastError: null });
try {
const result = await activityService.getMyActivities(params);
const items = result.items || [];
this.setState({
myActivities: items,
myActivitiesCursor: result.nextCursor || null,
myActivitiesHasMore: !!result.nextCursor,
myActivitiesLoading: false,
myActiveCount: items.filter(
(a) =>
a.registrationStatus === REGISTRATION_STATUS.APPROVED &&
a.status !== ACTIVITY_STATUS.ENDED &&
a.status !== ACTIVITY_STATUS.CANCELLED,
).length,
});
return result;
} catch (err) {
this.setState({ myActivitiesLoading: false, lastError: err });
throw err;
}
}
/** 加载更多我的活动 */
async loadMoreMyActivities() {
if (this.state.myActivitiesLoading || !this.state.myActivitiesCursor) return;
this.setState({ myActivitiesLoading: true });
try {
const result = await activityService.getMyActivities({
cursor: this.state.myActivitiesCursor,
pageSize: 20,
});
this.setState({
myActivities: [...this.state.myActivities, ...(result.items || [])],
myActivitiesCursor: result.nextCursor || null,
myActivitiesHasMore: !!result.nextCursor,
myActivitiesLoading: false,
});
} catch (err) {
this.setState({ myActivitiesLoading: false, lastError: err });
throw err;
}
}
/** 拉取活动详情(优先取缓存) */
async fetchActivityDetail(activityId, { force = false } = {}) {
if (!force && this.state.detailCache[activityId]) {
return this.state.detailCache[activityId];
}
const detail = await activityService.getActivityDetail(activityId);
this.setState({
detailCache: { ...this.state.detailCache, [activityId]: detail },
registrationStatusByActivity: {
...this.state.registrationStatusByActivity,
[activityId]: detail.registrationStatus,
},
});
return detail;
}
/**
* 报名活动
* @param {string} activityId
* @param {{ remark?: string }} [payload]
*/
async register(activityId, payload) {
const result = await activityService.register(activityId, payload);
this.setState({
registrationStatusByActivity: {
...this.state.registrationStatusByActivity,
[activityId]: REGISTRATION_STATUS.PENDING,
},
});
return result;
}
/**
* 提交活动反馈(含表达意向)
*
* 原有一个 `submitInterests(activityId, likedUserIds)` 已删除:它请求
* `POST /activities/{id}/interests` —— **后端没有这个端点**(Phase 16 把
* 表达意向合并进反馈流程,即本方法的 `interestTargetUserIds`),调用必然 404。
* 全仓已确认它没有任何页面调用方,属 Phase 16 前的设计残留。
* 若将来确实需要独立的"提交互选"接口,须由产品先定义语义再补后端端点,
* 不应让前端留一个指向 404 的方法。
*/
async submitFeedback(activityId, payload) {
return activityService.submitFeedback(activityId, payload);
}
}
const activityStore = new ActivityStore();
module.exports = activityStore;