Files
danyang-match/miniapp/pages/profile-photos/profile-photos.js
T
WorkBuddy ca0db57f24 chore(release): 冻结 v1.1.0 版本
- 同步版本号:VERSION / backend pom / admin-web / miniapp 均升至 1.1.0
- 合并本轮代码审查修复(详见 docs/CODE-REVIEW-2026-09-24.md):
  - 后端:微信登录真实实现 RealWechatLoginProvider、实名核验真实实现 RealIdentityVerificationProvider(本地强校验 + 可配置第三方 + fail-closed/fail-fast)
  - 照片直传落库端点 POST /me/photos/confirm(fileKey 归属校验 + 对象存在性校验后 savePhoto)
  - 刷新 Token Redis TTL 修正(getExpirationMs()*2 的 4h → refreshExpirationMs 的 14 天)
  - 管理端 token 改为 HttpOnly Cookie + silent-refresh,移除 localStorage 明文存储与 30+ 处手工注入
  - 小程序:红娘接单鉴权头错位修复、serviceType 中文映射、生产域名可配置
- 隔离本地产物(.workbuddy/、*.bundle、*.dump、secrets、application-dev.yml 等已被 .gitignore 覆盖)

未验证项(打标签 ≠ 生产发布):
- 后端编译 / 单测未在本环境执行(Maven 慢、Docker DNS 受限)
- 管理端 登录→刷新→登出 端到端、小程序真机走查待人工验收
- 生产部署需注入 WECHAT_APP_ID / WECHAT_APP_SECRET、VERIFICATION_API_URL / VERIFICATION_API_KEY 与小程序生产域名
2026-09-30 12:10:00 +08:00

214 lines
6.7 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 由服务端清理);带上 photoType 使 fileKey 路径与类型一致
const fileName = `photo_${Date.now()}.jpg`;
const tokenRes = await profileService.requestUploadToken({ fileName, contentType: 'image/jpeg', photoType });
// 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. 直传成功后必须调 confirm 落库,否则对象成孤儿、照片不展示(修复"照片直传不落库")
const confirmRes = await profileService.confirmPhoto({ fileKey: tokenRes.fileKey, photoType });
// 4. 若上传的是头像区,或当前无主头像,则设为主头像(用真实 photoId)
if (photoType === 'avatar' || this.data.avatarPhotos.length === 0) {
await profileService.setMainPhoto(confirmRes.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 });
},
});