- 同步版本号: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 与小程序生产域名
203 lines
6.8 KiB
JavaScript
203 lines
6.8 KiB
JavaScript
/**
|
||
* 用户资料服务(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,
|
||
};
|