Files
danyang-match/miniapp/pages/profile-confirm/profile-confirm.js
T

202 lines
7.3 KiB
JavaScript
Raw Normal View History

2026-09-09 22:05:00 +08:00
/**
* 资料确认页 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');
2026-09-09 22:05:00 +08:00
// 审核状态 → 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;
2026-09-09 22:05:00 +08:00
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)));
2026-09-09 22:05:00 +08:00
}
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),
2026-09-09 22:05:00 +08:00
});
} 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 });
},
});