/** * 资料确认页 U15(Phase 04 P04-T28) * * 重构要点(遵循 AGENTS.md 不变量): * - 数据:userStore.fetchProfile / fetchPreferences / fetchReviewStatus(第 9 条) * - 提交:userStore.submitReview(POST /me/profile/submit-review + Idempotency-Key) * - 草稿:提交成功后清除 ONBOARDING_DRAFT,避免回填污染(第 9 条:DB 是 Source of Truth) * - 审核状态展示:not_submitted / pending / approved / need_edit / rejected * - 资料完整度:客户端派生展示,最终由服务端校验,前端不放宽 Required(第 2 条) * - 路由:提交成功后 redirectTo 至 REVIEW.PENDING(Phase 05 已实现); * 审核中页 onShow 守卫会在状态变化时引导至合适页面。 */ const { userStore, appStore } = require('../../stores'); const { profileService } = require('../../services'); const { ROUTE, STORAGE_KEYS, PROFILE_REVIEW, profileLabelOf } = require('../../constants'); // 审核状态 → StatusBadge kind 映射 const REVIEW_BADGE = { [PROFILE_REVIEW.NOT_SUBMITTED]: { label: '未提交', kind: 'disabled' }, [PROFILE_REVIEW.PENDING]: { label: '审核中', kind: 'warning' }, [PROFILE_REVIEW.APPROVED]: { label: '已通过', kind: 'success' }, [PROFILE_REVIEW.NEED_EDIT]: { label: '需修改', kind: 'warning' }, [PROFILE_REVIEW.REJECTED]: { label: '未通过', kind: 'danger' }, }; /** * 计算资料完整度(客户端派生展示) * * 注意:仅用于 UI 进度条展示,最终是否可提交由服务端校验决定 * (AGENTS 第 9 条:PostgreSQL 是 Source of Truth)。 * @param {object} profile * @param {object} preferences * @param {number} photoCount * @returns {number} 0-100 */ function calcCompletion(profile, preferences, photoCount) { // AGENTS 第 9 条:PostgreSQL 是 Source of Truth —— 完成度由服务端计算 // 并随 GET /me/profile 返回(SelfProfileDTO.profileCompletion)。 // 此前前端用 22 条本地猜测式断言重算,且其中 hasChildren/occupationCategory/ // workRegion/activityWillingness/interests 等服务端并不返回 → 完成度恒偏低; // 现直接采用服务端值(保留形参以兼容既有调用方)。 void preferences; void photoCount; if (!profile) return 0; const serverValue = Number(profile.profileCompletion); if (!Number.isFinite(serverValue)) return 0; return Math.max(0, Math.min(100, Math.round(serverValue))); } Page({ data: { statusBarHeight: 20, navBarHeight: 44, loading: true, submitting: false, profile: null, preferences: null, photoCount: 0, reviewStatus: PROFILE_REVIEW.NOT_SUBMITTED, reviewComment: '', resubmitAllowed: true, completion: 0, reviewBadge: REVIEW_BADGE[PROFILE_REVIEW.NOT_SUBMITTED], // 派生展示字段 maritalLabel: '', educationLabel: '', childrenLabel: '', }, onLoad() { this._initSystemInfo(); this.loadAll(); }, _initSystemInfo() { try { const sysInfo = wx.getSystemInfoSync(); this.setData({ statusBarHeight: sysInfo.statusBarHeight || 20, navBarHeight: 44, }); } catch (e) { /* 使用默认值 */ } }, // ============ 数据加载 ============ async loadAll() { this.setData({ loading: true }); try { // 并行拉取资料/择偶条件/审核状态,照片列表用于计数 const [profile, preferences, review, photosRes] = await Promise.all([ userStore.fetchProfile().catch(() => null), userStore.fetchPreferences().catch(() => null), userStore.fetchReviewStatus().catch(() => null), profileService.getPhotos().catch(() => ({ photos: [] })), ]); const photoCount = (photosRes && photosRes.photos && photosRes.photos.length) || 0; // REST §39:服务端返回 { status, items[] } const reviewStatus = (review && review.status) || PROFILE_REVIEW.NOT_SUBMITTED; // 派生驳回原因摘要:取所有 need_edit/rejected 项的 reasonText 拼接 const rejectedItems = (review && review.items || []).filter( (i) => i.status === PROFILE_REVIEW.NEED_EDIT || i.status === PROFILE_REVIEW.REJECTED ); const reviewComment = rejectedItems .map((i) => i.reasonText) .filter(Boolean) .join(';'); // 仅 NEED_EDIT / REJECTED 允许重新提交(修改后再次提交) const resubmitAllowed = reviewStatus === PROFILE_REVIEW.NOT_SUBMITTED || reviewStatus === PROFILE_REVIEW.NEED_EDIT || reviewStatus === PROFILE_REVIEW.REJECTED; const completion = calcCompletion(profile, preferences, photoCount); this.setData({ profile, preferences, photoCount, reviewStatus, reviewComment, resubmitAllowed, completion, reviewBadge: REVIEW_BADGE[reviewStatus] || REVIEW_BADGE[PROFILE_REVIEW.NOT_SUBMITTED], maritalLabel: profileLabelOf('maritalStatus', profile && profile.maritalStatus) || '-', educationLabel: profileLabelOf('educationCode', profile && profile.educationCode) || '-', childrenLabel: profileLabelOf('childrenStatus', profile && profile.childrenStatus), }); } catch (err) { appStore.toastError(err, '加载失败'); } finally { this.setData({ loading: false }); } }, onRefresh() { this.loadAll(); }, // ============ 提交审核 ============ /** * 提交资料审核 * * AGENTS 第 2 条:Required 不可自动放宽——本页只读展示,不修改任何择偶条件, * 不存在前端放宽约束的路径。 * AGENTS 第 9 条:DB 是 Source of Truth——提交成功立即清除本地草稿, * 后续以服务端审核状态为准,本地不再回填。 */ async onSubmit() { if (this.data.submitting) return; if (!this.data.resubmitAllowed) { return appStore.toast('当前状态不可提交,请稍后或修改后再试'); } const ok = await appStore.confirm({ title: '确认提交审核?', content: '提交后资料将进入人工审核,审核期间您可继续修改,但需重新提交审核。', }); if (!ok) return; this.setData({ submitting: true }); try { await userStore.submitReview(); // 提交成功:清除新手草稿(避免再次进入回填旧数据) this._clearDraft(); appStore.toastSuccess('已提交审核'); // 跳转审核中页(Phase 05 已实现) setTimeout(() => { wx.redirectTo({ url: ROUTE.REVIEW.PENDING }); }, 800); } catch (err) { appStore.toastError(err, '提交失败'); } finally { this.setData({ submitting: false }); } }, // ============ 草稿清理 ============ _clearDraft() { try { wx.removeStorageSync(STORAGE_KEYS.ONBOARDING_DRAFT); wx.removeStorageSync(STORAGE_KEYS.ONBOARDING_CURRENT_STEP); } catch (e) { /* ignore */ } }, // ============ 导航 ============ onBack() { wx.navigateBack(); }, // 跳回指定资料页继续修改(审核驳回/需修改场景) onEditBasic() { wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_BASIC }); }, onEditIntro() { wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_INTRO }); }, onEditPreference() { wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_PREFERENCE }); }, });