chore: establish V1.1 recovery baseline

This commit is contained in:
2026-09-09 22:05:00 +08:00
commit b46b48eb33
947 changed files with 155471 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
/**
* 活动服务(API_INVENTORY 行 92-96, 98)
*
* 封装:活动列表 / 详情 / 报名 / 我的活动 / 反馈 / 互选意向提交。
* 关键不变量(AGENTS.md):
* - 第 6 条:活动单向意向保密——A 选 B 但 B 没选 → B 不知道。
* 前端调用 interests 提交后,结果接口只返回"匹配成功"的双向交集,
* 不返回对方是否单独选了我。
* - 第 1 条:用户永久免费——feeType=paid 仅作为后端字段,前端展示以"公益免费"为主标识。
* 报名与互选均走幂等键防重(AGENTS 第 12 条:参数配置化)。
*/
const api = require('../utils/api');
/**
* 活动列表(API_INVENTORY 行 92)
* @param {{ status?: string, cursor?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<import('../types').Activity>, nextCursor: string|null }>}
*/
function getActivities(params = {}) {
return api.get('/activities', params);
}
/**
* 活动详情(API_INVENTORY 行 93)
* @param {string} activityId
* @returns {Promise<import('../types').Activity & { registrationStatus: import('../types').RegistrationStatus }>}
*/
function getActivityDetail(activityId) {
return api.get(`/activities/${encodeURIComponent(activityId)}`);
}
/**
* 报名活动(API_INVENTORY 行 94)
* @param {string} activityId
* @param {{ remark?: string }} [payload]
* @returns {Promise<{ registrationStatus: 'pending', registrationId: string }>}
*/
function register(activityId, payload = {}) {
const idempotencyKey = `activity-register:${activityId}:${Date.now()}`;
return api.post(`/activities/${encodeURIComponent(activityId)}/register`, payload, undefined, idempotencyKey);
}
/**
* 我的活动(API_INVENTORY 行 95)
* @param {{ status?: string, cursor?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<import('../types').Activity & { registrationStatus: import('../types').RegistrationStatus }>, nextCursor: string|null }>}
*/
function getMyActivities(params = {}) {
return api.get('/my-activities', params);
}
/**
* 提交活动反馈(API_INVENTORY 行 96)
* @param {string} activityId
* @param {{ rating: number, content: string }} payload
* @returns {Promise<{ submitted: true }>}
*/
function submitFeedback(activityId, payload) {
const idempotencyKey = `activity-feedback:${activityId}:${Date.now()}`;
return api.post(`/activities/${encodeURIComponent(activityId)}/feedback`, payload, undefined, idempotencyKey);
}
/**
* 提交活动互选意向(API_INVENTORY 行 98,AGENTS 第 6 条:单向意向保密)
* @param {string} activityId
* @param {{ likedUserIds: string[] }} payload
* @returns {Promise<{ submitted: true, matchedUserIds: string[] }>}
* matchedUserIds 仅包含双向匹配成功的人;未匹配的人不会泄露是否被对方选择。
*/
function submitInterests(activityId, payload) {
const idempotencyKey = `activity-interests:${activityId}:${Date.now()}`;
return api.post(`/activities/${encodeURIComponent(activityId)}/interests`, payload, undefined, idempotencyKey);
}
module.exports = {
getActivities,
getActivityDetail,
register,
getMyActivities,
submitFeedback,
submitInterests,
};
+27
View File
@@ -0,0 +1,27 @@
/**
* App 配置服务(规范 §118 App 配置)
*
* 拉取服务端下发的业务数量参数(推荐人数、字数上限、Feature Flag 等),
* 与 constants/businessLimits.js 的默认值合并,覆盖运行时配置。
* 接口失败时保留默认值不变(规范 §119),保证核心流程可用。
*/
const api = require('../utils/api');
const { mergeAppConfig, BUSINESS_LIMITS } = require('../constants');
/**
* 拉取并合并服务端 App 配置
* @returns {Promise<typeof BUSINESS_LIMITS>} 合并后的运行时配置(失败时返回默认值)
*/
async function fetchAppConfig() {
try {
const overrides = await api.get('/app/config');
return mergeAppConfig(overrides);
} catch (err) {
// 配置接口失败不影响核心流程(规范 §119)
return BUSINESS_LIMITS;
}
}
module.exports = {
fetchAppConfig,
};
+37
View File
@@ -0,0 +1,37 @@
/**
* 申诉服务(REST §80 章 POST /appeals)
*
* 封装:提交申诉 / 我的申诉列表。
* 关键不变量(AGENTS.md):
* - 第 8 条:敏感数据默认拒绝——本层不接受身份证号、手机号等敏感字段,
* 申诉内容只接收 reason + 证据图片 URL(已上传至 OSS)。
* - 第 4 条:Block 优先级最高——申诉不会自动恢复账号或解除拉黑,
* 是否解封由运营人工处置。
*/
const api = require('../utils/api');
/**
* 提交申诉(REST §81 章)
* @param {{ appealType: 'ban_appeal'|'suspend_appeal', relatedActionId?: string, reason: string, evidenceFileIds?: string[], contactMethod?: string }} payload
* @returns {Promise<{ appealId: string, status: 'pending', submittedAt: number }>}
*/
function submitAppeal(payload) {
// 幂等键:申诉类型 + 关联处置 ID + 当前秒,防止用户多次点击重复提交
const actionKey = payload.relatedActionId || 'no_action';
const idempotencyKey = `appeal:${payload.appealType}:${actionKey}:${Math.floor(Date.now() / 1000)}`;
return api.post('/appeals', payload, undefined, idempotencyKey);
}
/**
* 我的申诉列表
* @param {{ cursor?: string, pageSize?: number, status?: string }} params
* @returns {Promise<{ items: Array<import('../types').Appeal>, nextCursor: string|null }>}
*/
function getMyAppeals(params = {}) {
return api.get('/me/appeals', params);
}
module.exports = {
submitAppeal,
getMyAppeals,
};
+51
View File
@@ -0,0 +1,51 @@
/**
* 认证服务(规范 §131 services 层 / API_INVENTORY 认证域)
*
* 仅封装 auth/wechat/login、auth/phone/bind、auth/refresh、auth/logout 四个端点。
* Token 持久化、Session 失效回调由 utils/api.js 统一处理;本层不读写本地存储。
* 刷新 Token 在 401 自动重试由 AuthStore 协调(单飞锁),见 stores/authStore。
*/
const api = require('../utils/api');
/**
* 微信登录(API_INVENTORY 行 8)
* @param {{ code: string }} payload wx.login 拿到的 code
* @returns {Promise<{ token: string, refreshToken: string, expiresIn: number, needPhone: boolean, needProfile: boolean }>}
*/
function wechatLogin(payload) {
return api.post('/auth/wechat/login', payload, undefined, payload.code);
}
/**
* 绑定手机号(API_INVENTORY 行 9)
* @param {{ phoneCode: string }} payload wx.getPhoneNumber 拿到的 encryptedData/iv 或 phoneCode
* @returns {Promise<{ bound: true }>}
*/
function bindPhone(payload) {
return api.post('/auth/phone/bind', payload, undefined, payload.phoneCode || payload.encryptedData);
}
/**
* 刷新 Token(API_INVENTORY 行 10)
* @returns {Promise<{ token: string, refreshToken: string, expiresIn: number }>}
*/
function refresh() {
const refreshToken = api.getRefreshToken();
return api.post('/auth/refresh', { refreshToken });
}
/**
* 登出(API_INVENTORY 行 11)
* 幂等:Idempotency-Key = "logout:" + 当前毫秒时间,便于后端去重。
* @returns {Promise<void>}
*/
function logout() {
return api.post('/auth/logout', {}, undefined, 'logout:' + Date.now());
}
module.exports = {
wechatLogin,
bindPhone,
refresh,
logout,
};
+37
View File
@@ -0,0 +1,37 @@
/**
* services 层聚合入口(规范 §130-131)
*
* 页面与 Store 统一从这里导入业务服务,不直接 require utils/api,
* 也不直接 require 各子服务模块——便于后续替换实现或加日志埋点。
*
* 用法:
* const { authService, matchService } = require('../services');
* await authService.wechatLogin({ code });
*/
const authService = require('./authService');
const userService = require('./userService');
const profileService = require('./profileService');
const verificationService = require('./verificationService');
const recommendationService = require('./recommendationService');
const matchService = require('./matchService');
const messageService = require('./messageService');
const activityService = require('./activityService');
const matchmakerService = require('./matchmakerService');
const safetyService = require('./safetyService');
const appealService = require('./appealService');
const appConfigService = require('./appConfigService');
module.exports = {
authService,
userService,
profileService,
verificationService,
recommendationService,
matchService,
messageService,
activityService,
matchmakerService,
safetyService,
appealService,
appConfigService,
};
+162
View File
@@ -0,0 +1,162 @@
/**
* 关系与匹配服务(API_INVENTORY 行 48-55, 60-63)
*
* 封装:Like/Pass/Block/取消/解除 + Mutual 列表 + Unmatch + 成功牵手双确认。
* 关键不变量(AGENTS.md):
* - 第 4 条:Block 即时禁止互动——前端调用 block 后必须立即更新本地会话与列表。
* - 第 5 条:红娘不可替用户 Like——本层仅供当前用户调用,不接受 matchmakerId 参数。
* - 第 6 条:成功牵手必须双确认——success/request 与 success/confirm 分两次调用。
* 单向意向不泄露:A Like B 但未 Mutual,本层不提供"谁喜欢我"的可识别列表,
* 仅返回计数 / 是否被 liked 的布尔,详情走 /relations/likers(仅返回匿名化字段)。
*/
const api = require('../utils/api');
/**
* 表达喜欢(API_INVENTORY 行 48)
* @param {string} userId
* @returns {Promise<import('../types').LikeResult>}
*/
function like(userId) {
const idempotencyKey = `like:${userId}:${Date.now()}`;
return api.post(`/relations/${encodeURIComponent(userId)}/like`, {}, undefined, idempotencyKey);
}
/**
* 取消喜欢(API_INVENTORY 行 49)
* 注意:Mutual 后不可直接取消(后端会返回 MATCH_REQUIRED),前端不应阻断调用。
* @param {string} userId
* @returns {Promise<{ relationStatus: import('../types').RelationStatus }>}
*/
function unlike(userId) {
return api.delete(`/relations/${encodeURIComponent(userId)}/like`, {}, undefined, `unlike:${userId}:${Date.now()}`);
}
/**
* 表示不合适(API_INVENTORY 行 50)
* Pass 是单向意向,不会通知对方(AGENTS 第 6 条)。
* @param {string} userId
* @returns {Promise<{ relationStatus: 'passed' }>}
*/
function pass(userId) {
const idempotencyKey = `pass:${userId}:${Date.now()}`;
return api.post(`/relations/${encodeURIComponent(userId)}/pass`, {}, undefined, idempotencyKey);
}
/**
* 拉黑用户(API_INVENTORY 行 51,AGENTS 第 4 条:Block 优先级最高)
* 前端立即停止与该用户的所有互动,从推荐/搜索/会话列表移除。
* @param {string} userId
* @param {{ reason?: string }} [payload]
* @returns {Promise<{ relationStatus: 'blocked' }>}
*/
function block(userId, payload = {}) {
const idempotencyKey = `block:${userId}:${Date.now()}`;
return api.post(`/relations/${encodeURIComponent(userId)}/block`, payload, undefined, idempotencyKey);
}
/**
* 解除拉黑(API_INVENTORY 行 52)
* @param {string} userId
* @returns {Promise<{ relationStatus: 'neutral' }>}
*/
function unblock(userId) {
return api.delete(`/relations/${encodeURIComponent(userId)}/block`, {}, undefined, `unblock:${userId}:${Date.now()}`);
}
/**
* 获取互相喜欢列表(API_INVENTORY 行 53,即 Mutual Matches)
* @param {{ cursor?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<import('../types').Conversation>, nextCursor: string|null }>}
*/
function getMutualList(params = {}) {
return api.get('/relations/mutual', params);
}
/**
* 获取我喜欢的人列表(API_INVENTORY 行 54)
* @param {{ cursor?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<import('../types').User & { likedAt: number }>, nextCursor: string|null }>}
*/
function getLikedByMe(params = {}) {
return api.get('/relations/liked', params);
}
/**
* 获取喜欢我的人列表(API_INVENTORY 行 55)
* 仅返回脱敏字段(昵称/年龄/地区/是否已 Mutual),不泄露对方明示身份。
* @param {{ cursor?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<import('../types').User>, nextCursor: string|null, totalCount: number }>}
*/
function getLikers(params = {}) {
return api.get('/relations/likers', params);
}
/**
* 获取黑名单列表(REST §86 章 GET /me/blacklist)
* 拉黑/解除拉黑统一走 Relationship 接口(block/unblock),本接口仅查询。
* 返回字段已脱敏(不返回对方手机号/身份证等敏感信息)。
* @param {{ cursor?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<{ userId: string, userName: string, avatarUrl?: string, blockedAt: number, reason?: string }>, nextCursor: string|null }>}
*/
function getBlockedList(params = {}) {
return api.get('/me/blacklist', params);
}
/**
* 获取我的匹配列表(API_INVENTORY 行 60)
* 与 /relations/mutual 区别:matches 包含牵手状态、服务跟进字段,会话列表专用。
* @param {{ cursor?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<import('../types').Conversation>, nextCursor: string|null }>}
*/
function getMatches(params = {}) {
return api.get('/matches', params);
}
/**
* 解除匹配(API_INVENTORY 行 61)
* 解除后双方会话立即停用,AGENTS 第 4 条影响范围仅次于 Block。
* @param {string} matchId
* @returns {Promise<{ relationStatus: 'unmatched' }>}
*/
function unmatch(matchId) {
const idempotencyKey = `unmatch:${matchId}:${Date.now()}`;
return api.post(`/matches/${encodeURIComponent(matchId)}/unmatch`, {}, undefined, idempotencyKey);
}
/**
* 发起成功牵手请求(API_INVENTORY 行 62,AGENTS 第 6 条:成功牵手必须双确认)
* 仅发起方调用一次,需对方在有效期内 confirm。
* @param {string} matchId
* @returns {Promise<{ successRequestId: string, expiresAt: number, status: 'pending_confirm' }>}
*/
function requestSuccess(matchId) {
const idempotencyKey = `success-req:${matchId}:${Date.now()}`;
return api.post(`/matches/${encodeURIComponent(matchId)}/success/request`, {}, undefined, idempotencyKey);
}
/**
* 确认成功牵手(API_INVENTORY 行 63)
* 仅被请求方调用,确认后双方关系置为 matched_successfully。
* @param {string} matchId
* @returns {Promise<{ relationshipStatus: 'matched_successfully', confirmedAt: number }>}
*/
function confirmSuccess(matchId) {
const idempotencyKey = `success-confirm:${matchId}:${Date.now()}`;
return api.post(`/matches/${encodeURIComponent(matchId)}/success/confirm`, {}, undefined, idempotencyKey);
}
module.exports = {
like,
unlike,
pass,
block,
unblock,
getMutualList,
getLikedByMe,
getLikers,
getBlockedList,
getMatches,
unmatch,
requestSuccess,
confirmSuccess,
};
+54
View File
@@ -0,0 +1,54 @@
/**
* 红娘服务(API_INVENTORY 行 102-105)
*
* 封装:申请红娘服务 / 我的服务列表 / 服务详情 / 红娘推荐列表。
* 关键不变量(AGENTS.md):
* - 第 5 条:红娘不可替用户 Like——红娘只产出推荐,最终 Like/Pass 由用户本人操作。
* 本层不提供任何"代用户操作"的接口;推荐操作走 matchService.like/pass。
* - 第 7 条:普通工作人员不能查看私人聊天——本层无聊天接入入口。
*/
const api = require('../utils/api');
/**
* 申请红娘服务(API_INVENTORY 行 102)
* @param {{ requirement: string }} payload 用户对自己择偶需求的进一步描述
* @returns {Promise<{ serviceId: string, status: 'submitted' }>}
*/
function applyService(payload) {
const idempotencyKey = `matchmaker-apply:${Date.now()}`;
return api.post('/matchmaker/services', payload, undefined, idempotencyKey);
}
/**
* 我的服务列表(API_INVENTORY 行 103)
* @param {{ cursor?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<import('../types').MatchmakerService>, nextCursor: string|null }>}
*/
function getMyServices(params = {}) {
return api.get('/matchmaker/services', params);
}
/**
* 服务详情(API_INVENTORY 行 104)
* @param {string} serviceId
* @returns {Promise<import('../types').MatchmakerService>}
*/
function getServiceDetail(serviceId) {
return api.get(`/matchmaker/services/${encodeURIComponent(serviceId)}`);
}
/**
* 红娘推荐列表(API_INVENTORY 行 105)
* @param {{ serviceId?: string, cursor?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<import('../types').MatchmakerRecommendation>, nextCursor: string|null }>}
*/
function getRecommendations(params = {}) {
return api.get('/matchmaker/recommendations', params);
}
module.exports = {
applyService,
getMyServices,
getServiceDetail,
getRecommendations,
};
+111
View File
@@ -0,0 +1,111 @@
/**
* 消息服务(API_INVENTORY 行 75-79, 84-87)
*
* 封装:会话列表 / 详情 / 历史消息 / 发送 / 未读数 + 通知列表 / 标记已读。
* 关键不变量(AGENTS.md):
* - 第 3 条:Mutual Match 后才能聊天——后端会返回 MATCH_REQUIRED,
* 前端在 send 失败时给出对应引导,不在 services 层做客户端校验。
* - 第 4 条:Block/Unmatch 后会话 canSendMessage=false,send 直接拒绝。
* - 第 7 条:工作人员不可看私人聊天——本层无任何 staff 入口。
* 消息发送走幂等键 messageId(规范 §106),防止弱网下重复入库。
*/
const api = require('../utils/api');
/**
* 获取会话列表(API_INVENTORY 行 75)
* @param {{ cursor?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<import('../types').Conversation>, nextCursor: string|null }>}
*/
function getConversations(params = {}) {
return api.get('/conversations', params);
}
/**
* 获取会话详情(API_INVENTORY 行 76)
* @param {string} conversationId
* @returns {Promise<import('../types').Conversation>}
*/
function getConversation(conversationId) {
return api.get(`/conversations/${encodeURIComponent(conversationId)}`);
}
/**
* 发送消息(API_INVENTORY 行 77)
* @param {string} conversationId
* @param {{ content: string, messageId: string, clientSentAt: number }} payload
* @returns {Promise<{ messageId: string, sentAt: number, status: 'sent' }>}
*/
function sendMessage(conversationId, payload) {
// 幂等键 = 客户端生成的 messageId(规范 §106),后端去重
return api.post(
`/conversations/${encodeURIComponent(conversationId)}/messages`,
payload,
undefined,
payload.messageId,
);
}
/**
* 获取消息历史(API_INVENTORY 行 78)
* @param {string} conversationId
* @param {{ before?: string, after?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<import('../types').Message>, nextCursor: string|null, hasMore: boolean }>}
*/
function getMessages(conversationId, params = {}) {
return api.get(`/conversations/${encodeURIComponent(conversationId)}/messages`, params);
}
/**
* 获取会话未读数(API_INVENTORY 行 79)
* @param {string} conversationId
* @returns {Promise<{ unreadCount: number }>}
*/
function getUnreadCount(conversationId) {
return api.get(`/conversations/${encodeURIComponent(conversationId)}/unread`);
}
/**
* 获取通知列表(API_INVENTORY 行 84)
* @param {{ cursor?: string, pageSize?: number, type?: string }} params
* @returns {Promise<{ items: Array<any>, nextCursor: string|null }>}
*/
function getNotifications(params = {}) {
return api.get('/notifications', params);
}
/**
* 标记单条通知已读(API_INVENTORY 行 85)
* @param {string} notificationId
* @returns {Promise<{ read: true }>}
*/
function markNotificationRead(notificationId) {
return api.put(`/notifications/${encodeURIComponent(notificationId)}/read`, {}, undefined, `notif-read:${notificationId}:${Date.now()}`);
}
/**
* 全部通知已读(API_INVENTORY 行 86)
* @returns {Promise<{ updated: number }>}
*/
function markAllNotificationsRead() {
return api.put('/notifications/read-all', {}, undefined, 'notif-read-all:' + Date.now());
}
/**
* 通知未读总数(API_INVENTORY 行 87)
* @returns {Promise<{ unreadCount: number }>}
*/
function getNotificationsUnreadCount() {
return api.get('/notifications/unread-count');
}
module.exports = {
getConversations,
getConversation,
sendMessage,
getMessages,
getUnreadCount,
getNotifications,
markNotificationRead,
markAllNotificationsRead,
getNotificationsUnreadCount,
};
+189
View File
@@ -0,0 +1,189 @@
/**
* 用户资料服务(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 = 'profile-put:' + (payload && payload.userId || 'me') + ':' + Date.now();
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 = 'preferences-put:' + Date.now();
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 = 'photo-main:' + photoId + ':' + Date.now();
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);
}
/**
* 获取隐私设置(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 = 'privacy-put:' + Date.now();
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 = 'review-submit:' + Date.now();
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 = 'rel-pause:' + Date.now();
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 = 'rel-resume:' + Date.now();
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 = 'rel-gtk:' + Date.now();
return api.patch('/me/relationship-status', { status: 'getting_to_know' }, undefined, idempotencyKey);
}
module.exports = {
getProfile,
updateProfile,
getPreferences,
updatePreferences,
getPhotos,
deletePhoto,
setMainPhoto,
requestUploadToken,
getPrivacySettings,
updatePrivacySettings,
getReviewStatus,
submitReview,
getRelationshipStatus,
pauseSearching,
resumeSearching,
markGettingToKnow,
};
+51
View File
@@ -0,0 +1,51 @@
/**
* 推荐与搜索服务(API_INVENTORY 行 36-37, 41-43)
*
* 封装:今日推荐列表、推荐详情、Discover 搜索、用户公开主页。
* Required 不可自动放宽(AGENTS 第 2 条)——本层不实现任何"放宽条件后再查"的逻辑,
* 仅把请求透传给后端,由后端按 PartnerPreference 做硬约束过滤。
*/
const api = require('../utils/api');
/**
* 获取今日推荐列表(API_INVENTORY 行 36)
* @param {{ cursor?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<import('../types').Recommendation>, nextCursor: string|null, dailyRemaining: number }>}
*/
function getRecommendations(params = {}) {
return api.get('/recommendations', params);
}
/**
* 获取推荐详情(API_INVENTORY 行 37)
* @param {string} userId
* @returns {Promise<import('../types').Recommendation>}
*/
function getRecommendationDetail(userId) {
return api.get(`/recommendations/${encodeURIComponent(userId)}`);
}
/**
* Discover 搜索(API_INVENTORY 行 42)
* @param {{ keyword?: string, region?: string, ageMin?: number, ageMax?: number, cursor?: string, pageSize?: number }} params
* @returns {Promise<{ items: Array<import('../types').User>, nextCursor: string|null }>}
*/
function discover(params = {}) {
return api.get('/discover', params);
}
/**
* 查看用户公开主页(API_INVENTORY 行 43)
* @param {string} userId
* @returns {Promise<import('../types').User & { relationStatus: import('../types').RelationStatus }>}
*/
function getUserDetail(userId) {
return api.get(`/users/${encodeURIComponent(userId)}`);
}
module.exports = {
getRecommendations,
getRecommendationDetail,
discover,
getUserDetail,
};
+40
View File
@@ -0,0 +1,40 @@
/**
* 举报与安全服务(API_INVENTORY 行 110-111)
*
* 封装:提交举报 / 我的举报列表。
* 关键不变量(AGENTS.md):
* - 第 8 条:敏感数据默认拒绝——本层不接受身份证号、手机号等敏感字段,
* 举报内容只接收 reason + 证据图片 URL(已上传至 OSS)。
* - 第 4 条:Block 优先级最高——举报不会自动 Block,是否 Block 由用户主动调用
* matchService.block;举报后由运营人工处置。
*/
const api = require('../utils/api');
/**
* 提交举报(REST §81 章 POST /reports)
*
* 请求体字段对齐 REST 规范:targetUserId / reportType / description / evidenceFileIds。
* evidenceFileIds 为已上传至 OSS 的文件 ID 列表(由调用方先上传图片获取 fileId)。
*
* @param {{ targetUserId: string, reportType: import('../types').ReportType, description: string, evidenceFileIds?: string[] }} payload
* @returns {Promise<{ reportId: string, status: 'pending', submittedAt: number }>}
*/
function submitReport(payload) {
// 幂等键:目标用户 + 举报类型 + 当前秒,防止用户多次点击重复提交
const idempotencyKey = `report:${payload.targetUserId}:${payload.reportType}:${Math.floor(Date.now() / 1000)}`;
return api.post('/reports', payload, undefined, idempotencyKey);
}
/**
* 我的举报列表(REST §80 章 GET /me/reports)
* @param {{ cursor?: string, pageSize?: number, status?: string }} params
* @returns {Promise<{ items: Array<import('../types').Report>, nextCursor: string|null }>}
*/
function getMyReports(params = {}) {
return api.get('/me/reports', params);
}
module.exports = {
submitReport,
getMyReports,
};
+19
View File
@@ -0,0 +1,19 @@
/**
* 用户状态服务(API_INVENTORY 行 12)
*
* 仅封装 /me/status:返回全局 UserState 聚合,决定路由、权限与首页 Tab。
* /me/profile、/me/preferences 等单独走 profileService。
*/
const api = require('../utils/api');
/**
* 获取当前用户状态(API_INVENTORY 行 12)
* @returns {Promise<import('../types').UserState>}
*/
function getStatus() {
return api.get('/me/status');
}
module.exports = {
getStatus,
};
+33
View File
@@ -0,0 +1,33 @@
/**
* 实名认证服务(API_INVENTORY 行 30-31)
*
* 封装提交与查询两个端点。
* 提交走 POST + 幂等键(同一身份证不可重复提交)。
* 身份证号等敏感字段由 stores/userStore 在收集阶段加密传输,本层不缓存明文。
*/
const api = require('../utils/api');
/**
* 提交实名认证(API_INVENTORY 行 30)
* @param {{ realName: string, idCardNumber: string, faceImageUrl?: string }} payload
* @returns {Promise<{ submitted: true, status: 'pending' }>}
*/
function submit(payload) {
// 幂等键:身份证号后4位 + 当前秒,避免用户多次点击
const tail = (payload.idCardNumber || '').slice(-4).toUpperCase();
const idempotencyKey = 'verification-submit:' + tail + ':' + Math.floor(Date.now() / 1000);
return api.post('/verification/submit', payload, undefined, idempotencyKey);
}
/**
* 查询认证状态(API_INVENTORY 行 31)
* @returns {Promise<{ status: 'not_submitted'|'pending'|'approved'|'rejected', rejectReason?: string, submittedAt?: number, reviewedAt?: number }>}
*/
function getStatus() {
return api.get('/verification/status');
}
module.exports = {
submit,
getStatus,
};