chore: establish V1.1 recovery baseline
This commit is contained in:
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user