Files
danyang-match/miniapp/services/profileService.js
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

203 lines
6.8 KiB
JavaScript
Raw Permalink 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.
/**
* 用户资料服务(API_INVENTORY 行 17-25, 68-70)
*
* 封装:本人资料、择偶条件、生活照、隐私设置、审核状态、关系状态切换。
* 严格区分 GET 与 PUT,避免误操作;PUT 调用走幂等键防重。
* 自我介绍/择偶条件字数限制由 stores/profileStore 校验,本层只负责传输。
*/
const api = require('../utils/api');
/**
* 获取本人资料(API_INVENTORY 行 17)
* @returns {Promise<import('../types').Profile>}
*/
function getProfile() {
return api.get('/me/profile');
}
/**
* 更新本人资料(API_INVENTORY 行 18)
* @param {Partial<import('../types').Profile>} payload
* @returns {Promise<import('../types').Profile>} 服务端返回的最新资料
*/
function updateProfile(payload) {
// PUT 默认幂等:用 userId + 字段指纹作为幂等键,避免重复提交时脏写
const idempotencyKey = api.idempotencyKey('profile-put', (payload && payload.userId) || 'me');
return api.put('/me/profile', payload, undefined, idempotencyKey);
}
/**
* 获取择偶条件(API_INVENTORY 行 19)
* @returns {Promise<import('../types').PartnerPreference>}
*/
function getPreferences() {
return api.get('/me/preferences');
}
/**
* 更新择偶条件(API_INVENTORY 行 20)
* @param {Partial<import('../types').PartnerPreference>} payload
* @returns {Promise<import('../types').PartnerPreference>}
*/
function updatePreferences(payload) {
const idempotencyKey = api.idempotencyKey('preferences-put');
return api.put('/me/preferences', payload, undefined, idempotencyKey);
}
/**
* 获取生活照列表(API_INVENTORY 行 21)
* @returns {Promise<{ photos: Array<{ photoId: string, url: string, status: string }> }>}
*/
function getPhotos() {
return api.get('/me/photos');
}
/**
* 删除生活照(API_INVENTORY 行 21b)
* @param {string} photoId
* @returns {Promise<void>}
*/
function deletePhoto(photoId) {
return api.delete(`/me/photos/${photoId}`);
}
/**
* 设置主头像(将指定照片设为主头像)
* @param {string} photoId
* @returns {Promise<{ avatarUrl: string }>}
*/
function setMainPhoto(photoId) {
const idempotencyKey = api.idempotencyKey('photo-main', photoId);
return api.put(`/me/photos/${photoId}/main`, {}, undefined, idempotencyKey);
}
/**
* 申请照片上传凭证(API_INVENTORY 行 22)
* @param {{ fileName: string, contentType?: string }} payload
* @returns {Promise<{ uploadUrl: string, photoId: string, expiresAt: number, accessUrl: string }>}
*/
function requestUploadToken(payload) {
return api.post('/me/photos/upload-token', payload, undefined, payload.fileName);
}
/**
* 确认直传照片落库(对应 REST §29 补:POST /me/photos/confirm)
* 客户端直传对象存储成功后必须调用,否则对象成孤儿、照片不展示、签名访问也过不了归属校验。
* @param {{ fileKey: string, photoType: string }} payload
* @returns {Promise<{ photoId: string, photoType: string }>}
*/
function confirmPhoto(payload) {
// 以 fileKey 作幂等指纹:同一文件重复确认只落库一次(直传凭证可重试)
const idempotencyKey = api.idempotencyKey('photo-confirm', payload.fileKey);
return api.post('/me/photos/confirm', payload, undefined, idempotencyKey);
}
/**
* 获取隐私设置(API_INVENTORY 行 23)
* @returns {Promise<import('../types').PrivacySettings>}
*/
function getPrivacySettings() {
return api.get('/me/privacy-settings');
}
/**
* 更新隐私设置(API_INVENTORY 行 24)
* @param {Partial<import('../types').PrivacySettings>} payload
* @returns {Promise<import('../types').PrivacySettings>}
*/
function updatePrivacySettings(payload) {
const idempotencyKey = api.idempotencyKey('privacy-put');
return api.put('/me/privacy-settings', payload, undefined, idempotencyKey);
}
/**
* 获取审核状态(API_INVENTORY 行 25,REST §36-39)
*
* 服务端返回结构:
* {
* status: 'not_submitted' | 'pending' | 'approved' | 'need_edit' | 'rejected',
* items: [
* { itemType: 'avatar'|'self_intro'|'basic'|'lifestyle'|'intent'|'preference'|'interests',
* status: 'pending'|'approved'|'need_edit'|'rejected',
* reasonCode?: string, // 驳回时存在,如 AVATAR_NOT_CLEAR
* reasonText?: string } // 驳回时存在,用户可读文案
* ],
* submittedAt?: number,
* reviewedAt?: number
* }
*
* @returns {Promise<{ status: string, items: Array<{itemType:string,status:string,reasonCode?:string,reasonText?:string}>, submittedAt?: number, reviewedAt?: number }>}
*/
function getReviewStatus() {
return api.get('/me/profile/review-status');
}
/**
* 提交资料审核(REST §37,POST /me/profile/submit-review)
*
* 幂等:用 Idempotency-Key 防止网络重试导致重复生成审核任务。
* 服务端校验资料完整度、敏感词、实名认证前置条件后生成 ReviewTask。
*
* @returns {Promise<{ reviewStatus: 'pending', submittedAt: number }>}
*/
function submitReview() {
const idempotencyKey = api.idempotencyKey('review-submit');
return api.post('/me/profile/submit-review', {}, undefined, idempotencyKey);
}
/**
* 获取婚恋关系状态(REST §83 章 GET /me/relationship-status)
* @returns {Promise<{ status: string, recommendationEnabled: boolean, matchingWith?: { matchId: string, displayName: string } }>}
*/
function getRelationshipStatus() {
return api.get('/me/relationship-status');
}
/**
* 暂停寻找(REST §84 章 PATCH /me/relationship-status)
* 成功牵手不能通过此接口直接设置(后端返回 SUCCESS_CONFIRMATION_REQUIRED)。
* @returns {Promise<{ status: 'paused', recommendationEnabled: boolean }>}
*/
function pauseSearching() {
const idempotencyKey = api.idempotencyKey('rel-pause');
return api.patch('/me/relationship-status', { status: 'paused' }, undefined, idempotencyKey);
}
/**
* 恢复寻找(REST §84 章 PATCH /me/relationship-status)
* @returns {Promise<{ status: 'searching', recommendationEnabled: boolean }>}
*/
function resumeSearching() {
const idempotencyKey = api.idempotencyKey('rel-resume');
return api.patch('/me/relationship-status', { status: 'searching' }, undefined, idempotencyKey);
}
/**
* 设置为"正在了解"(REST §84 章 PATCH /me/relationship-status)
* @returns {Promise<{ status: 'getting_to_know', recommendationEnabled: boolean }>}
*/
function markGettingToKnow() {
const idempotencyKey = api.idempotencyKey('rel-gtk');
return api.patch('/me/relationship-status', { status: 'getting_to_know' }, undefined, idempotencyKey);
}
module.exports = {
getProfile,
updateProfile,
getPreferences,
updatePreferences,
getPhotos,
deletePhoto,
setMainPhoto,
requestUploadToken,
confirmPhoto,
getPrivacySettings,
updatePrivacySettings,
getReviewStatus,
submitReview,
getRelationshipStatus,
pauseSearching,
resumeSearching,
markGettingToKnow,
};