/** * stores 聚合入口(规范 §131) * * 暴露 7 个 Store 单例 + 跨 Store 编排辅助函数 + 启动初始化 bootstrap。 * * 跨 Store 编排原则: * - 单个 Store 自身只维护自己的状态,不直接 require 其他 Store(避免循环依赖)。 * - 涉及多 Store 副作用的动作(如 Block 后会话停用、Mutual 后会话刷新)通过本文件 * 暴露的辅助函数编排,调用方(页面)使用这些辅助函数而非直接组合多个 Store。 * * 用法(页面): * const { matchStore, appStore, blockUser } = require('../../stores'); * await blockUser(userId, '骚扰'); */ const authStore = require('./authStore'); const userStore = require('./userStore'); const matchStore = require('./matchStore'); const messageStore = require('./messageStore'); const activityStore = require('./activityStore'); const matchmakerStore = require('./matchmakerStore'); const appStore = require('./appStore'); const { ROUTE, PROFILE_REVIEW, ACCOUNT_STATUS, buildPath } = require('../constants'); // ============ Review 状态路由守卫(P05-T12,规范 §4.3) ============ // // AGENTS 第 9 条:PostgreSQL 是 Source of Truth——守卫只读 userStore.state.userState, // 不预判、不缓存;状态变化时由各页 onShow 调用 requireApproved 自检并跳转。 // // 受保护路由分级: // - 'approved':资料审核通过后方可访问(推荐/找对象/缘分中心/用户详情/活动报名) // - 'searching':关系态为 searching 方可访问(部分推荐入口) // - 'logged_in':仅需登录(消息/我的/设置等) // 审核未通过时一律引导至 REVIEW.PENDING 或 REVIEW.REJECTED。 /** * 受保护路由 → 所需最低审核态映射 * 未列出的路由默认为 'logged_in'(仅需登录) */ const ROUTE_ACCESS_REQUIREMENTS = { // 推荐 / 找对象(PROFILE_NOT_APPROVED 错误码兜底,但客户端预拦截体验更好) [ROUTE.TAB.DISCOVER]: 'approved', [ROUTE.MAIN.DISCOVER]: 'approved', [ROUTE.MAIN.FATE_CENTER]: 'approved', [ROUTE.DETAIL.PROFILE_DETAIL]: 'approved', [ROUTE.DETAIL.ACTIVITY_REGISTER]: 'approved', // 红娘申请需审核通过 [ROUTE.DETAIL.MATCHMAKER_APPLY]: 'approved', }; /** * 读取当前用户的审核态(来自 userState.profileReviewStatus,仅本地读取) * @returns {string} PROFILE_REVIEW 值;未登录或无状态时返回 NOT_SUBMITTED */ function readReviewStatus() { return userStore.state.userState?.profileReviewStatus || PROFILE_REVIEW.NOT_SUBMITTED; } /** * 根据当前审核态决定应跳转的"审核态页面" * @returns {string|null} 需要跳转的路径;approved 返回 null */ function getReviewRedirectPath() { const status = readReviewStatus(); if (status === PROFILE_REVIEW.PENDING) return ROUTE.REVIEW.PENDING; if (status === PROFILE_REVIEW.NEED_EDIT || status === PROFILE_REVIEW.REJECTED) { return ROUTE.REVIEW.REJECTED; } return null; } /** * 校验当前用户是否可访问指定路径 * @param {string} path 路由常量 * @returns {{ok: boolean, redirect?: string}} ok=true 可访问;ok=false 时 redirect 为应跳转路径 */ function canAccess(path) { const required = ROUTE_ACCESS_REQUIREMENTS[path]; if (!required || required === 'logged_in') return { ok: true }; const reviewStatus = readReviewStatus(); const accountStatus = userStore.state.userState?.accountStatus || ACCOUNT_STATUS.NORMAL; // 账号异常态优先拦截 if (accountStatus !== ACCOUNT_STATUS.NORMAL) { return { ok: false, redirect: ROUTE.REVIEW.ACCOUNT_STATUS }; } if (required === 'approved' && reviewStatus !== PROFILE_REVIEW.APPROVED) { const redirect = getReviewRedirectPath(); return { ok: false, redirect: redirect || ROUTE.REVIEW.PENDING }; } return { ok: true }; } /** * 守卫式导航:检查审核态后再决定跳转目标 * * 用法: * const { guardedNavigate } = require('../../stores'); * guardedNavigate(ROUTE.MAIN.DISCOVER); // 未审核则自动跳 review-pending * * @param {string} path 目标路由常量 * @param {Record} [params] 仅 id 类参数 * @returns {boolean} 是否成功发起导航(true=已跳转到目标或审核页) */ function guardedNavigate(path, params) { const access = canAccess(path); if (access.ok) { const target = params ? buildPath(path, params) : path; wx.navigateTo({ url: target }); return true; } if (access.redirect) { wx.redirectTo({ url: access.redirect }); } return false; } /** * 当前用户是否已通过资料审核 * @returns {boolean} */ function isProfileApproved() { return readReviewStatus() === PROFILE_REVIEW.APPROVED; } // ============ 跨 Store 编排 ============ /** * 拉黑用户:触发 MatchStore 与 MessageStore 同步副作用 * - MatchStore:从推荐/Mutual 列表移除该用户 * - MessageStore:标记对应会话不可发消息(AGENTS 第 4 条) * @param {string} userId * @param {string} [reason] */ async function blockUser(userId, reason) { await matchStore.block(userId, reason); // 查找对应会话并标记停用 const conv = messageStore.state.conversations.find( (c) => c.targetUserId === userId || c.conversationId === userId, ); if (conv) messageStore.markConversationBlocked(conv.conversationId); } /** * Like 用户:若返回 mutualMatch=true,自动刷新会话列表 * @param {string} userId * @returns {Promise} */ async function likeUser(userId) { const result = await matchStore.like(userId); if (result && result.mutualMatch) { // Mutual Match 达成,刷新会话列表,让用户能立即开始聊天 await messageStore.fetchConversations(); } return result; } /** * 解除匹配:触发 MatchStore 与 MessageStore 同步副作用 * @param {string} matchId */ async function unmatchConversation(matchId) { await matchStore.unmatch(matchId); messageStore.markConversationBlocked(matchId); } /** * 成功牵手:双确认第二步(仅被请求方调用),成功后刷新 userStore 与会话列表 * @param {string} matchId */ async function confirmSuccess(matchId) { const result = await matchStore.confirmSuccess(matchId); // 成功牵手后关系状态变化,需刷新 userState try { await userStore.fetchStatus(); } catch (e) { /* 不阻塞主流程 */ } try { await messageStore.fetchConversations(); } catch (e) { /* 同上 */ } return result; } // ============ 启动初始化 ============ /** * App.onLaunch 调用,按依赖顺序初始化所有 Store * 1. appStore.init() — 拉取服务端配置(不阻塞核心) * 2. authStore.hydrate() — 从本地恢复登录态 * 3. userStore.hydrate() — 从本地恢复用户状态快照 * 4. messageStore.hydrate() — 从本地恢复未读数(兜底) * 5. 若已登录,异步刷新 userStore.fetchStatus()(不阻塞启动) */ async function bootstrap() { // 同步阶段:本地状态恢复(避免白屏) authStore.hydrate(); userStore.hydrate(); messageStore.hydrate(); // 异步阶段:服务端配置 + 用户状态刷新 // 配置失败不阻塞启动;状态失败由路由守卫处理(跳登录页) appStore.init().catch(() => {}); if (authStore.state.isLoggedIn) { userStore.fetchStatus().catch(() => {}); } else { // 未登录:跳转到登录页(使用 navigateTo 而非 redirectTo,因为首页是 Tab 页) setTimeout(() => { try { wx.navigateTo({ url: ROUTE.AUTH.LOGIN }); } catch (e) { console.warn('[bootstrap] 跳转登录页失败:', e); } }, 300); } } module.exports = { // 单例 Store authStore, userStore, matchStore, messageStore, activityStore, matchmakerStore, appStore, // 跨 Store 编排辅助 blockUser, likeUser, unmatchConversation, confirmSuccess, // Review 状态路由守卫(P05-T12) readReviewStatus, getReviewRedirectPath, canAccess, guardedNavigate, isProfileApproved, ROUTE_ACCESS_REQUIREMENTS, // 启动入口 bootstrap, };