Files
danyang-match/miniapp/pages/profile-photos/profile-photos.js
T
WorkBuddy 438b38d121 V1.0.1 配置与规则纠偏:照片上限配置化+强校验 / 活体开关闭环 / 推荐阈值快照+规则版本化 / governance 三表+漂移审计
根治三类工程治理问题(技术默认值≠产品规则、配置存在但未闭环、文档代码漂移):
- PROFILE-001:profile.photo.maxCount(默认6未确认)+ ProfilePolicy + 服务器端 FOR UPDATE
  并发强校验(PHOTO_LIMIT_EXCEEDED,拒绝先于 S3 不留孤儿)+ 前端 mergeAppConfig 嵌套键修复
- CFG-001/IDENTITY-001:IdentityPolicy + GET /verification/liveness-status 真实判定点
  (默认关不阻断,开则 enhancedVerified 变化,消除假开关)+ 删死 seed livenessVerification.enabled
- MATCH-001:阈值60定为技术默认(消解 PENDING 矛盾)+ V050 recommendation_records.threshold_snapshot
  规则版本化 + 删死重复配置类 MatchConfig
- DOC-001:新增 docs/governance/(config-register/open-questions/decision-register/
  hardcoded-rule-audit/config-drift-audit)
- TEST-001:新增守卫 PhotoLimitGuardTest(8)/IdentityLivenessGuardTest(5)/
  RecommendationThresholdSnapshotGuardTest(4),均经负向注入可红

全量回归 733/0/0/3(基线717+17新增-1删死测试);UI 契约门禁 18/18;STATUS 升 rc.21
2026-09-21 02:05:15 +08:00

211 lines
6.4 KiB
JavaScript
Raw 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.
/**
* 头像与照片页 U08(Phase 04 P04-T23)
*
* 重构要点(遵循 AGENTS.md 不变量 + Phase 04 验收条件):
* - 数据:profileService.getPhotos(每次从服务端获取,不在本地缓存图片 URL —— 验收"图片URL非永久公开")
* - 上传:profileService.requestUploadToken + wx.uploadFile(EXIF 由服务端清理,前端不处理)
* - 删除:profileService.deletePhoto
* - 主头像:profileService.setMainPhoto(第一张或用户指定)
* - 数量限制:BUSINESS_LIMITS.MAX_PROFILE_PHOTOS(第 12 条,不硬编码)
* - 敏感数据:本页不涉及身份证/手机号(第 8 条)
* - 下一步条件:至少 1 张主头像
*/
const { appStore } = require('../../stores');
const { profileService } = require('../../services');
const { ROUTE, BUSINESS_LIMITS } = require('../../constants');
Page({
data: {
statusBarHeight: 20,
navBarHeight: 44,
loading: false,
saving: false,
avatarPhotos: [],
lifestylePhotos: [],
maxLifestyle: BUSINESS_LIMITS.MAX_PROFILE_PHOTOS - 1,
// 页面说明
guidelines: [
'请使用本人近期真实照片',
'禁止网图、二维码、广告、联系方式图片',
'建议真人生活照,避免过度美颜',
],
},
onLoad() {
this._initSystemInfo();
this._applyPhotoLimit();
this.loadPhotos();
},
onShow() {
// 服务端配置(appStore.businessConfig)可能在冷启动后才拉取完成,onShow 再取一次
this._applyPhotoLimit();
},
/**
* 照片上限读运行时配置(服务端下发,经 appStore.businessConfig 覆盖本地默认),
* 前端不写死 6(PROFILE-001)。生活照槽位 = 总上限 - 1(头像占 1 张)。
*/
_applyPhotoLimit() {
const maxTotal = (appStore.state.businessConfig && appStore.state.businessConfig.MAX_PROFILE_PHOTOS)
|| BUSINESS_LIMITS.MAX_PROFILE_PHOTOS;
this.setData({ maxLifestyle: Math.max(0, maxTotal - 1) });
},
_initSystemInfo() {
try {
const sysInfo = wx.getSystemInfoSync();
this.setData({
statusBarHeight: sysInfo.statusBarHeight || 20,
navBarHeight: 44,
});
} catch (e) { /* 使用默认值 */ }
},
async loadPhotos() {
this.setData({ loading: true });
try {
const result = await profileService.getPhotos();
const photos = (result && result.photos) || [];
const avatarPhotos = photos.filter((p) => p.photoType === 'avatar');
const lifestylePhotos = photos.filter((p) => p.photoType === 'lifestyle');
this.setData({ avatarPhotos, lifestylePhotos, loading: false });
} catch (err) {
this.setData({ loading: false });
appStore.toastError(err, '加载照片失败');
}
},
// ============ 上传 ============
/** 主头像区添加 */
onAddAvatar() {
this._chooseAndUpload('avatar');
},
/** 生活照区添加 */
onAddLifestyle() {
const remaining = this.data.maxLifestyle - this.data.lifestylePhotos.length;
if (remaining <= 0) {
appStore.toast(`最多上传 ${this.data.maxLifestyle + 1} 张照片`);
return;
}
this._chooseAndUpload('lifestyle');
},
_chooseAndUpload(photoType) {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
sizeType: ['compressed'],
success: (res) => {
const tempFile = res.tempFiles[0];
if (!tempFile) return;
this._uploadPhoto(tempFile.tempFilePath, photoType);
},
fail: () => { /* 用户取消 */ },
});
},
async _uploadPhoto(filePath, photoType) {
appStore.showLoading('上传中…');
try {
// 1. 获取上传凭证(EXIF 由服务端清理)
const fileName = `photo_${Date.now()}.jpg`;
const tokenRes = await profileService.requestUploadToken({ fileName, contentType: 'image/jpeg' });
// 2. 上传到对象存储
await new Promise((resolve, reject) => {
wx.uploadFile({
url: tokenRes.uploadUrl,
filePath,
name: 'file',
formData: { 'Content-Type': 'image/jpeg' },
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) resolve(res);
else reject(new Error('上传失败'));
},
fail: reject,
});
});
// 3. 若上传的是头像区,或当前无主头像,则设为主头像
if (photoType === 'avatar' || this.data.avatarPhotos.length === 0) {
await profileService.setMainPhoto(tokenRes.photoId);
}
appStore.hideLoading();
appStore.toastSuccess('上传成功');
this.loadPhotos();
} catch (err) {
appStore.hideLoading();
appStore.toastError(err, '上传失败');
}
},
// ============ 删除 ============
async onRemoveAvatar(e) {
const { index } = e.detail;
const photo = this.data.avatarPhotos[index];
if (!photo) return;
this._removePhoto(photo.photoId);
},
async onRemoveLifestyle(e) {
const { index } = e.detail;
const photo = this.data.lifestylePhotos[index];
if (!photo) return;
this._removePhoto(photo.photoId);
},
async _removePhoto(photoId) {
const confirm = await appStore.confirm('确定删除这张照片?', '删除后不可恢复');
if (!confirm) return;
appStore.showLoading('删除中…');
try {
await profileService.deletePhoto(photoId);
appStore.hideLoading();
appStore.toastSuccess('已删除');
this.loadPhotos();
} catch (err) {
appStore.hideLoading();
appStore.toastError(err, '删除失败');
}
},
// ============ 预览 ============
onPreviewAvatar(e) {
const { index } = e.detail;
const photo = this.data.avatarPhotos[index];
if (photo) this._preview(photo.url);
},
onPreviewLifestyle(e) {
const { index } = e.detail;
const photo = this.data.lifestylePhotos[index];
if (photo) this._preview(photo.url);
},
_preview(currentUrl) {
const urls = [
...this.data.avatarPhotos.map((p) => p.url),
...this.data.lifestylePhotos.map((p) => p.url),
].filter(Boolean);
if (urls.length === 0) return;
wx.previewImage({ urls, current: currentUrl });
},
// ============ 导航 ============
onBack() {
wx.navigateBack();
},
onNext() {
if (this.data.avatarPhotos.length === 0) {
appStore.toast('请至少上传 1 张主头像');
return;
}
wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_INTERESTS });
},
});