release: freeze demo v1.1.0 2026-10-19
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s

This commit is contained in:
WorkBuddy
2026-10-19 20:36:22 +08:00
parent 8b505d9133
commit 69ebf33e18
157 changed files with 21111 additions and 72 deletions
+174
View File
@@ -0,0 +1,174 @@
import axios, { type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'
import { getItem, setItem, removeItem, StorageKeys } from '@/utils/storage'
import { releaseIdempotencyKey, generateRequestIdForHeader } from './idempotency'
export class ApiError extends Error {
code: string
requestId?: string
status?: number
constructor(code: string, message: string, requestId?: string, status?: number) {
super(message)
this.code = code
this.requestId = requestId
this.status = status
}
}
export interface RequestOptions {
idempotencyKey?: string
silent?: boolean
}
/** 会话失效回调(由 authStore 注入)。 */
export let onSessionExpired: (() => void) | null = null
export function setOnSessionExpired(fn: (() => void) | null): void {
onSessionExpired = fn
}
/** 维护态回调(由 appStore 注入)。 */
export let onMaintenance: ((body: any) => void) | null = null
export function setOnMaintenance(fn: ((body: any) => void) | null): void {
onMaintenance = fn
}
const client = axios.create({
baseURL: '/api/v1',
timeout: 15000,
})
function isAuthEndpoint(url?: string): boolean {
return typeof url === 'string' && url.indexOf('/auth/') === 0
}
client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = getItem(StorageKeys.accessToken)
config.headers = config.headers || {}
config.headers['X-Request-Id'] = generateRequestIdForHeader()
if (token) config.headers['Authorization'] = `Bearer ${token}`
return config
})
let refreshPromise: Promise<boolean> | null = null
/** 单飞刷新:并发 401 共享同一次刷新请求。refresh 只走 body,不依赖 Cookie。 */
async function refreshToken(): Promise<boolean> {
if (refreshPromise) return refreshPromise
refreshPromise = (async () => {
const refreshTokenValue = getItem(StorageKeys.refreshToken)
const userId = getItem(StorageKeys.userId)
if (!refreshTokenValue) return false
try {
const resp = await axios.post('/api/v1/auth/refresh', { userId, refreshToken: refreshTokenValue })
const body = resp.data || {}
if (body.success && body.data?.accessToken) {
setItem(StorageKeys.accessToken, body.data.accessToken)
if (body.data.refreshToken) setItem(StorageKeys.refreshToken, body.data.refreshToken)
return true
}
return false
} catch {
return false
} finally {
refreshPromise = null
}
})()
return refreshPromise
}
client.interceptors.response.use(
(resp) => {
const config = resp.config as InternalAxiosRequestConfig & { __idempotencyKey?: string }
const body = resp.data || {}
if (body.success) {
const data = body.data
// 先取值,再做收尾:释放幂等键失败绝不能升级为业务失败(对齐 miniapp 语义)。
try {
releaseIdempotencyKey(config.__idempotencyKey)
} catch {
/* ignore */
}
return data
}
throw new ApiError(body.code || 'BUSINESS_ERROR', body.message || '操作失败', body.requestId, resp.status)
},
async (error) => {
const resp = error?.response
if (!resp) {
throw new ApiError('NETWORK_ERROR', '网络连接失败,请检查网络后重试', undefined, undefined)
}
const body = resp.data || {}
const config = resp.config as InternalAxiosRequestConfig & { __retried?: boolean }
if (resp.status === 503) {
if (typeof onMaintenance === 'function') onMaintenance(body)
throw new ApiError('MAINTENANCE', '平台正在维护,请稍后再试', body.requestId, 503)
}
if (resp.status === 401) {
// 认证端点自身 401 不触发刷新(否则自我递归);由调用方处理。
if (!isAuthEndpoint(config?.url) && !config?.__retried) {
const ok = await refreshToken()
if (ok) {
config.__retried = true
const token = getItem(StorageKeys.accessToken)
if (token) config.headers['Authorization'] = `Bearer ${token}`
return client.request(config)
}
removeItem(StorageKeys.accessToken)
removeItem(StorageKeys.refreshToken)
if (typeof onSessionExpired === 'function') onSessionExpired()
}
throw new ApiError(body.code || 'UNAUTHORIZED', body.message || '请重新登录', body.requestId, 401)
}
throw new ApiError(body.code || 'SERVER_ERROR', body.message || '服务暂时不可用', body.requestId, resp.status)
},
)
export interface ApiOptions {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
data?: any
headers?: Record<string, string>
idempotencyKey?: string
}
export async function request<T = any>(url: string, options: ApiOptions = {}): Promise<T> {
const config: AxiosRequestConfig & { __idempotencyKey?: string } = {
url,
method: options.method || 'GET',
data: options.data,
headers: options.headers,
}
config.__idempotencyKey = options.idempotencyKey
return client.request(config) as Promise<T>
}
export function get<T = any>(url: string, params?: Record<string, any>): Promise<T> {
return client.get(url, { params }) as Promise<T>
}
export function post<T = any>(url: string, data?: any, opts: RequestOptions = {}): Promise<T> {
const config: AxiosRequestConfig & { __idempotencyKey?: string } = { url, method: 'POST', data }
config.__idempotencyKey = opts.idempotencyKey
return client.request(config) as Promise<T>
}
export function put<T = any>(url: string, data?: any, opts: RequestOptions = {}): Promise<T> {
const config: AxiosRequestConfig & { __idempotencyKey?: string } = { url, method: 'PUT', data }
config.__idempotencyKey = opts.idempotencyKey
return client.request(config) as Promise<T>
}
export function patch<T = any>(url: string, data?: any, opts: RequestOptions = {}): Promise<T> {
const config: AxiosRequestConfig & { __idempotencyKey?: string } = { url, method: 'PATCH', data }
config.__idempotencyKey = opts.idempotencyKey
return client.request(config) as Promise<T>
}
export function del<T = any>(url: string, data?: any, opts: RequestOptions = {}): Promise<T> {
const config: AxiosRequestConfig & { __idempotencyKey?: string } = { url, method: 'DELETE', data }
config.__idempotencyKey = opts.idempotencyKey
return client.request(config) as Promise<T>
}
export default client
+45
View File
@@ -0,0 +1,45 @@
/**
* 幂等键托管(复刻 miniapp/utils/api.js 语义)
*
* 语义:同一次**尚未成功**的逻辑操作复用同一把键;成功后释放,下一次点击才是新的一次。
* 「先 resolve 再收尾」——释放失败绝不能升级为业务失败(见 client.ts)。
* 未成功操作的键保留 10 分钟。
*/
const IDEMPOTENCY_KEY_TTL_MS = 10 * 60 * 1000
interface PendingEntry {
key: string
createdAt: number
}
const pending = new Map<string, PendingEntry>()
function generateRequestId(): string {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8)
}
export function idempotencyKey(scope: string, subject?: string | number | null): string {
const fingerprint = `${scope}::${subject == null ? '' : subject}`
const now = Date.now()
const cached = pending.get(fingerprint)
if (cached && now - cached.createdAt < IDEMPOTENCY_KEY_TTL_MS) {
return cached.key
}
const key = `${scope}:${subject == null ? '' : subject}:${generateRequestId()}`
pending.set(fingerprint, { key, createdAt: now })
return key
}
export function releaseIdempotencyKey(key?: string): void {
if (!key) return
for (const [fingerprint, entry] of pending.entries()) {
if (entry.key === key) {
pending.delete(fingerprint)
return
}
}
}
export function generateRequestIdForHeader(): string {
return generateRequestId()
}
+89
View File
@@ -0,0 +1,89 @@
import axios, { type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'
import { getItem, setItem, removeItem, StorageKeys } from '@/utils/storage'
import { ApiError } from './client'
/**
* 红娘/工作人员端独立请求实例。
*
* 与用户端 client 的关键区别(见实施计划 R3):
* - Bearer 取自 h5.operatorToken(与用户 accessToken 隔离);
* - 刷新一律走请求体 `POST /auth/operator/refresh { operatorId, refreshToken }`,
* **绝不读 Cookie** —— 避免与同浏览器内的 admin-web 会话串号。
*/
const operatorClient = axios.create({
baseURL: '/api/v1',
timeout: 15000,
})
operatorClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = getItem(StorageKeys.operatorToken)
config.headers = config.headers || {}
config.headers['X-Request-Id'] = Date.now().toString(36) + Math.random().toString(36).substring(2, 8)
if (token) config.headers['Authorization'] = `Bearer ${token}`
return config
})
let refreshing: Promise<boolean> | null = null
async function refreshOperatorToken(): Promise<boolean> {
if (refreshing) return refreshing
refreshing = (async () => {
const refreshToken = getItem(StorageKeys.refreshToken + '.operator')
const operatorId = getItem(StorageKeys.operatorId)
if (!refreshToken) return false
try {
const resp = await axios.post('/api/v1/auth/operator/refresh', { operatorId, refreshToken })
const body = resp.data || {}
if (body.success && body.data?.accessToken) {
setItem(StorageKeys.operatorToken, body.data.accessToken)
if (body.data.refreshToken) setItem(StorageKeys.refreshToken + '.operator', body.data.refreshToken)
return true
}
return false
} catch {
return false
} finally {
refreshing = null
}
})()
return refreshing
}
operatorClient.interceptors.response.use(
(resp) => {
const body = resp.data || {}
if (body.success) return body.data
throw new ApiError(body.code || 'BUSINESS_ERROR', body.message || '操作失败', body.requestId, resp.status)
},
async (error) => {
const resp = error?.response
if (!resp) throw new ApiError('NETWORK_ERROR', '网络连接失败,请重试')
const body = resp.data || {}
const config = resp.config as InternalAxiosRequestConfig & { __retried?: boolean }
if (resp.status === 401 && !config?.__retried) {
const ok = await refreshOperatorToken()
if (ok) {
config.__retried = true
const token = getItem(StorageKeys.operatorToken)
if (token) config.headers['Authorization'] = `Bearer ${token}`
return operatorClient.request(config)
}
removeItem(StorageKeys.operatorToken)
removeItem(StorageKeys.refreshToken + '.operator')
}
throw new ApiError(body.code || 'SERVER_ERROR', body.message || '服务暂时不可用', body.requestId, resp.status)
},
)
export function opGet<T = any>(url: string, params?: Record<string, any>): Promise<T> {
return operatorClient.get(url, { params }) as Promise<T>
}
export function opPost<T = any>(url: string, data?: any, opts: { idempotencyKey?: string } = {}): Promise<T> {
const config: AxiosRequestConfig & { __idempotencyKey?: string } = { url, method: 'POST', data }
return operatorClient.request(config) as Promise<T>
}
export function opPut<T = any>(url: string, data?: any): Promise<T> {
return operatorClient.put(url, data) as Promise<T>
}
export default operatorClient
+74
View File
@@ -0,0 +1,74 @@
import { get, post, del } from '../client'
import { idempotencyKey } from '../idempotency'
/** 活动列表/详情/报名/我的活动/反馈/签到 */
export const activityService = {
getActivities(params: Record<string, any> = {}) {
return get('/activities', params)
},
getActivityDetail(activityId: string) {
return get(`/activities/${encodeURIComponent(activityId)}`)
},
register(activityId: string, payload: Record<string, any> = {}) {
return post(`/activities/${encodeURIComponent(activityId)}/register`, payload, {
idempotencyKey: idempotencyKey('activity-register', activityId),
})
},
cancelRegistration(activityId: string) {
return del(`/activities/${encodeURIComponent(activityId)}/register`)
},
getMyActivities(params: Record<string, any> = {}) {
// 后端实际路由为 GET /activities/my(原 /my-activities 无对应 handler → 404)
return get('/activities/my', params)
},
submitFeedback(activityId: string, payload: Record<string, any>) {
return post(`/activities/${encodeURIComponent(activityId)}/feedback`, payload, {
idempotencyKey: idempotencyKey('activity-feedback', activityId),
})
},
generateCheckinToken(activityId: string) {
return post(`/activities/${encodeURIComponent(activityId)}/checkin-token`, {}, {
idempotencyKey: idempotencyKey('checkin-token', activityId),
})
},
getMutualMatches(activityId: string) {
return get(`/activities/${encodeURIComponent(activityId)}/interests/mutual`)
},
checkMutualInterest(activityId: string, partnerId: string) {
return get(`/activities/${encodeURIComponent(activityId)}/interests/mutual/${encodeURIComponent(partnerId)}`)
},
}
/**
* 活动互选二阶段同意(INV-014/015):
* 互选结果 → 双方各自 confirm → 才创建 Match/沟通入口。
* 单向意向保密:结果接口只返回双相交集。
*/
export const activityMutualService = {
getResults(activityId: string) {
return get(`/activities/${encodeURIComponent(activityId)}/mutual/results`)
},
createConsent(activityId: string, partnerId: string) {
return post(`/activities/${encodeURIComponent(activityId)}/mutual/consent`, { partnerId }, {
idempotencyKey: idempotencyKey('mutual-consent', `${activityId}:${partnerId}`),
})
},
confirmConnection(activityId: string, partnerId: string) {
return post(`/activities/${encodeURIComponent(activityId)}/mutual/confirm`, { partnerId }, {
idempotencyKey: idempotencyKey('mutual-confirm', `${activityId}:${partnerId}`),
})
},
declineConnection(activityId: string, partnerId: string) {
return post(`/activities/${encodeURIComponent(activityId)}/mutual/decline`, { partnerId }, {
idempotencyKey: idempotencyKey('mutual-decline', `${activityId}:${partnerId}`),
})
},
getMyConsentStatus(activityId: string, partnerId: string) {
return get(`/activities/${encodeURIComponent(activityId)}/mutual/status/${encodeURIComponent(partnerId)}`)
},
confirmInterest(activityId: string, partnerId: string) {
return post(`/activities/${encodeURIComponent(activityId)}/interests/confirm`, { partnerId }, {
idempotencyKey: idempotencyKey('interest-confirm', `${activityId}:${partnerId}`),
})
},
}
+47
View File
@@ -0,0 +1,47 @@
import { get, post } from '../client'
import { setItem, removeItem, StorageKeys } from '@/utils/storage'
/** 用户端认证(对齐 miniapp/services/authService.js) */
export const authService = {
wechatLogin(payload: { code: string }) {
return post('/auth/wechat/login', payload, { idempotencyKey: payload.code })
},
bindPhone(payload: Record<string, any>) {
return post('/auth/phone/bind', payload, { idempotencyKey: payload.phoneCode || payload.encryptedData })
},
refresh(userId?: string | null, refreshToken?: string | null) {
return post('/auth/refresh', { userId, refreshToken })
},
logout() {
return post('/auth/logout', {}, { idempotencyKey: 'logout' })
},
}
/** 红娘/工作人员端认证(独立会话,Bearer 走 h5.operatorToken) */
export const operatorService = {
async login(username: string, password: string): Promise<any> {
const data = await post('/auth/operator/login', { username, password })
if (data?.accessToken) {
setItem(StorageKeys.operatorToken, data.accessToken)
if (data.refreshToken) setItem(StorageKeys.refreshToken + '.operator', data.refreshToken)
if (data.operatorId) setItem(StorageKeys.operatorId, data.operatorId)
if (data.name) setItem(StorageKeys.operatorName, data.name)
}
return data
},
refresh(operatorId?: string | null, refreshToken?: string | null) {
return post('/auth/operator/refresh', { operatorId, refreshToken })
},
logout() {
return post('/auth/operator/logout', {}).catch(() => {})
},
clearSession() {
removeItem(StorageKeys.operatorToken)
removeItem(StorageKeys.refreshToken + '.operator')
removeItem(StorageKeys.operatorId)
removeItem(StorageKeys.operatorName)
},
get token(): string | null {
return localStorage.getItem('h5.' + StorageKeys.operatorToken)
},
}
+14
View File
@@ -0,0 +1,14 @@
export { authService, operatorService } from './auth'
export { userService, profileService, verificationService, localEligibilityService, notificationService, serviceHistoryService } from './me'
export { recommendationService, matchService, safetyService, appealService } from './match'
export { activityService, activityMutualService } from './activity'
export {
serviceAppointmentService,
serviceStationService,
serviceProgramService,
contentService,
counselingService,
} from './service'
export { matchmakerService, operatorMatchmakerService, referralService, contactConsentService } from './matchmaker'
export { homeService, capabilityService, appConfigService } from './misc'
export { ApiError } from '../client'
+103
View File
@@ -0,0 +1,103 @@
import { get, post, del } from '../client'
import { idempotencyKey } from '../idempotency'
/** 推荐与搜索 */
export const recommendationService = {
getRecommendations(params: Record<string, any> = {}) {
return get('/recommendations', params)
},
getRecommendationDetail(userId: string) {
return get(`/recommendations/${encodeURIComponent(userId)}`)
},
discover(params: Record<string, any> = {}) {
return get('/discover', params)
},
getUserDetail(userId: string) {
return get(`/discover/users/${encodeURIComponent(userId)}`)
},
}
/** 关系与匹配(Like/Pass/Block/缘分三榜/Unmatch/成功牵手双确认) */
export const matchService = {
like(userId: string) {
return post('/likes', { targetUserId: userId }, { idempotencyKey: idempotencyKey('like', userId) })
},
unlike(userId: string) {
return del(`/likes/${encodeURIComponent(userId)}`)
},
pass(userId: string) {
return post('/passes', { targetUserId: userId }, { idempotencyKey: idempotencyKey('pass', userId) })
},
block(userId: string, payload: { reason?: string } = {}) {
return post('/blocks', { targetUserId: userId, reason: payload.reason }, { idempotencyKey: idempotencyKey('block', userId) })
},
unblock(userId: string) {
return del(`/blocks/${encodeURIComponent(userId)}`)
},
getMutualList(params: Record<string, any> = {}) {
return get('/relations/fate/mutual', params)
},
getLikedByMe(params: Record<string, any> = {}) {
return get('/relations/fate/liked', params)
},
getLikers(params: Record<string, any> = {}) {
return get('/relations/fate/likers', params)
},
getBlockedList(params: Record<string, any> = {}) {
return get('/blocks', params)
},
getMatches(params: Record<string, any> = {}) {
return get('/matches', params)
},
unmatch(matchId: string) {
return post(`/matches/${encodeURIComponent(matchId)}/unmatch`, {}, { idempotencyKey: idempotencyKey('unmatch', matchId) })
},
requestSuccess(matchId: string) {
return post(`/matches/${encodeURIComponent(matchId)}/success/request`, {}, { idempotencyKey: idempotencyKey('success-req', matchId) })
},
confirmSuccess(matchId: string) {
return post(`/matches/${encodeURIComponent(matchId)}/success/confirm`, {}, { idempotencyKey: idempotencyKey('success-confirm', matchId) })
},
declineSuccess(matchId: string) {
return post(`/matches/${encodeURIComponent(matchId)}/success/decline`, {}, { idempotencyKey: idempotencyKey('success-decline', matchId) })
},
}
/** 举报(不自动 Block) */
export const safetyService = {
submitReport(payload: { targetUserId: string; reportType: string; description: string; evidenceFileIds?: string[] }) {
// 后端真实路由 POST /risk/reports,DTO 字段为 reportedUserId / reasonType(原 /reports + targetUserId/reportType 会导致 404)
return post(
'/risk/reports',
{
reportedUserId: payload.targetUserId,
reasonType: payload.reportType,
description: payload.description,
evidenceFileIds: payload.evidenceFileIds,
},
{ idempotencyKey: idempotencyKey('report', `${payload.targetUserId}:${payload.reportType}`) },
)
},
getMyReports(params: Record<string, any> = {}) {
return get('/me/reports', params)
},
/** 举报详情(GET /risk/reports/{reportId},仅举报人本人可见):返回 { report, evidenceFileKeys, evidenceUrls } */
getReportDetail(reportId: string) {
return get(`/risk/reports/${encodeURIComponent(reportId)}`)
},
/** 举报证据直传凭证(POST /risk/reports/evidence/upload-token):返回 { fileKey, uploadUrl, expiresIn } */
requestEvidenceUploadToken() {
return post('/risk/reports/evidence/upload-token', {})
},
}
/** 申诉 */
export const appealService = {
submitAppeal(payload: Record<string, any>) {
const actionKey = payload.relatedActionId || 'no_action'
return post('/appeals', payload, { idempotencyKey: idempotencyKey('appeal', `${payload.appealType}:${actionKey}`) })
},
getMyAppeals(params: Record<string, any> = {}) {
return get('/me/appeals', params)
},
}
+127
View File
@@ -0,0 +1,127 @@
import { get, post, put } from '../client'
import { opGet, opPost } from '../operatorClient'
import { idempotencyKey } from '../idempotency'
/** 用户端红娘服务(申请/我的服务/详情/推荐) */
export const matchmakerService = {
applyService(payload: Record<string, any>) {
return post('/matchmaker/services', payload, { idempotencyKey: idempotencyKey('matchmaker-apply') })
},
getMyServices(params: Record<string, any> = {}) {
return get('/matchmaker/services', params)
},
async getServiceDetail(serviceId: string) {
// 用户端后端未注册 GET /matchmaker/services/{id}(仅列表接口)——
// 由列表按 id 派生,避免对不存在路由发起 404 请求。
const result: any = await get('/matchmaker/services', { limit: 50 })
const list: any[] = result?.items || result?.records || (Array.isArray(result) ? result : [])
return list.find((s: any) => (s.serviceId || s.id) === serviceId) ?? null
},
confirmService(serviceId: string) {
return post(`/matchmaker/services/${encodeURIComponent(serviceId)}/confirm`, {}, {
idempotencyKey: idempotencyKey('matchmaker-confirm', serviceId),
})
},
withdrawService(serviceId: string) {
return post(`/matchmaker/services/${encodeURIComponent(serviceId)}/withdraw`, {}, {
idempotencyKey: idempotencyKey('matchmaker-withdraw', serviceId),
})
},
getRecommendations(params: Record<string, any> = {}) {
return get('/matchmaker/recommendations', params)
},
markRecommendationViewed(recommendationId: string) {
return put(`/matchmaker/recommendations/${encodeURIComponent(recommendationId)}/viewed`)
},
acceptRecommendation(recommendationId: string) {
return post(`/matchmaker/recommendations/${encodeURIComponent(recommendationId)}/accept`, {}, {
idempotencyKey: idempotencyKey('mm-rec-accept', recommendationId),
})
},
passRecommendation(recommendationId: string) {
return post(`/matchmaker/recommendations/${encodeURIComponent(recommendationId)}/pass`, {}, {
idempotencyKey: idempotencyKey('mm-rec-pass', recommendationId),
})
},
}
/**
* 红娘工作台(Operator 会话)。
* 不变量:红娘只做推荐/跟进,**不可替用户 Like/Pass**(AGENTS 第 5 条)。
*/
export const operatorMatchmakerService = {
dashboard() {
return opGet('/operator/matchmaker/dashboard')
},
myAssignments() {
return opGet('/operator/matchmaker/assignments/my')
},
recommendations() {
return opGet('/operator/matchmaker/recommendations')
},
pending() {
return opGet('/operator/matchmaker/pending')
},
managementServices() {
return opGet('/operator/matchmaker/management/services')
},
serviceDetail(id: string) {
return opGet(`/operator/matchmaker/services/${encodeURIComponent(id)}`)
},
recommend(serviceId: string, payload: Record<string, any>) {
return opPost(`/operator/matchmaker/services/${encodeURIComponent(serviceId)}/recommend`, payload)
},
followup(serviceId: string, payload: Record<string, any>) {
return opPost(`/operator/matchmaker/services/${encodeURIComponent(serviceId)}/followup`, payload)
},
complete(serviceId: string) {
return opPost(`/operator/matchmaker/services/${encodeURIComponent(serviceId)}/complete`, {})
},
myContactAuthorizations() {
return opGet('/operator/matchmaker/contact-authorizations/my')
},
requestContactAuthorization(payload: Record<string, any>) {
return opPost('/operator/matchmaker/contact-authorizations', payload)
},
revokeContactAuthorization(id: string, reason: string) {
return opPost(`/operator/matchmaker/contact-authorizations/${encodeURIComponent(id)}/revoke`, { reason })
},
referralHistory() {
return opGet('/referrals/operator/history')
},
referToServiceStation(payload: Record<string, any>) {
return opPost('/referrals/service-station', payload)
},
referToActivity(payload: Record<string, any>) {
return opPost('/referrals/activity', payload)
},
}
/** 红娘转介(用户侧确认) */
export const referralService = {
getMyReferralHistory() {
return get('/referrals/my-history')
},
confirmReferral(targetId: string, targetType: 'service_station' | 'activity') {
return post(`/referrals/${encodeURIComponent(targetId)}/confirm`, { targetType }, {
idempotencyKey: idempotencyKey('referral-confirm', `${targetId}:${targetType}`),
})
},
}
/** 受控联系授权(用户单独明示同意,INV) */
export const contactConsentService = {
my() {
return get('/contact-authorizations/my')
},
consent(id: string) {
return post(`/contact-authorizations/${encodeURIComponent(id)}/consent`, {}, {
idempotencyKey: idempotencyKey('contact-consent', id),
})
},
decline(id: string) {
return post(`/contact-authorizations/${encodeURIComponent(id)}/decline`, {}, {
idempotencyKey: idempotencyKey('contact-decline', id),
})
},
}
+121
View File
@@ -0,0 +1,121 @@
import { get, post, put, patch, del } from '../client'
import { idempotencyKey } from '../idempotency'
/** 本人全局状态(GET /me/status) */
export const userService = {
getStatus() {
return get('/me/status')
},
}
/** 本人资料 / 择偶条件 / 照片 / 隐私 / 审核 / 关系状态(对齐 miniapp/services/profileService.js) */
export const profileService = {
getProfile() {
return get('/me/profile')
},
updateProfile(payload: Record<string, any>) {
return put('/me/profile', payload, { idempotencyKey: idempotencyKey('profile-put', payload?.userId || 'me') })
},
updateBasicProfile(payload: Record<string, any>) {
return patch('/me/profile/basic', payload, { idempotencyKey: idempotencyKey('profile-basic', 'me') })
},
getCompletion() {
return get('/me/profile/completion')
},
getPreferences() {
return get('/me/preferences')
},
updatePreferences(payload: Record<string, any>) {
return put('/me/preferences', payload, { idempotencyKey: idempotencyKey('preferences-put') })
},
getPhotos() {
return get('/me/photos')
},
deletePhoto(photoId: string) {
return del(`/me/photos/${encodeURIComponent(photoId)}`)
},
setMainPhoto(photoId: string) {
return put(`/me/photos/${encodeURIComponent(photoId)}/main`, {}, { idempotencyKey: idempotencyKey('photo-main', photoId) })
},
requestUploadToken(payload: { fileName: string; contentType?: string }) {
return post('/me/photos/upload-token', payload, { idempotencyKey: payload.fileName })
},
confirmPhoto(payload: { fileKey: string; photoType: string }) {
return post('/me/photos/confirm', payload, { idempotencyKey: idempotencyKey('photo-confirm', payload.fileKey) })
},
getPrivacySettings() {
return get('/me/privacy-settings')
},
updatePrivacySettings(payload: Record<string, any>) {
return put('/me/privacy-settings', payload, { idempotencyKey: idempotencyKey('privacy-put') })
},
getReviewStatus() {
return get('/me/profile/review-status')
},
submitReview() {
return post('/me/profile/submit-review', {}, { idempotencyKey: idempotencyKey('review-submit') })
},
getRelationshipStatus() {
return get('/me/relationship-status')
},
pauseSearching() {
return patch('/me/relationship-status', { status: 'paused' }, { idempotencyKey: idempotencyKey('rel-pause') })
},
resumeSearching() {
return patch('/me/relationship-status', { status: 'searching' }, { idempotencyKey: idempotencyKey('rel-resume') })
},
markGettingToKnow() {
return patch('/me/relationship-status', { status: 'getting_to_know' }, { idempotencyKey: idempotencyKey('rel-gtk') })
},
}
/** 实名认证 */
export const verificationService = {
submit(payload: { realName: string; idCardNumber: string; faceImageUrl?: string }) {
const tail = (payload.idCardNumber || '').slice(-4).toUpperCase()
return post('/verification/submit', payload, { idempotencyKey: idempotencyKey('verification-submit', tail) })
},
getStatus() {
return get('/verification/status')
},
}
/** 本地资格核验 */
export const localEligibilityService = {
getStatus() {
return get('/local-eligibility')
},
declare(payload: Record<string, any>) {
return post('/local-eligibility/declaration', payload)
},
uploadEvidence(payload: Record<string, any>) {
return post('/local-eligibility/evidences', payload)
},
submit() {
return post('/local-eligibility/submit', {}, { idempotencyKey: idempotencyKey('local-eligibility-submit') })
},
}
/** 通知(用户端不提供聊天,仅通知) */
export const notificationService = {
getNotifications(params: Record<string, any> = {}) {
return get('/notifications', params)
},
markNotificationRead(id: string) {
return put(`/notifications/${encodeURIComponent(id)}/read`)
},
markAllNotificationsRead() {
return put('/notifications/read-all')
},
getNotificationsUnreadCount() {
// 后端实际路由为 GET /notifications/unread(返回 {count})
return get('/notifications/unread')
},
}
/** 公益服务记录(我的服务时间线) */
export const serviceHistoryService = {
getMyServiceHistory() {
return get('/me/service-history')
},
}
+27
View File
@@ -0,0 +1,27 @@
import { get } from '../client'
/** 首页聚合(GET /home)—— 段序与 8 入口顺序由后端返回决定(AGENTS 第 14/15 条)。 */
export const homeService = {
getHome() {
return get('/home')
},
}
/** 能力开关(AGENTS 第 26 条:入口可见性只读服务端) */
export const capabilityService = {
getCapabilities() {
return get('/capabilities')
},
}
/** App 运行时配置(数量/阈值等,未确认参数一律配置化) */
export const appConfigService = {
async fetchAppConfig(fallback: Record<string, any> = {}) {
try {
const overrides = await get('/app/config')
return { ...fallback, ...(overrides || {}) }
} catch {
return fallback
}
},
}
+104
View File
@@ -0,0 +1,104 @@
import { get, post } from '../client'
import { idempotencyKey } from '../idempotency'
/** 服务预约 */
export const serviceAppointmentService = {
async getCurrentServiceLocation() {
try {
const home: any = await get('/home')
const location = home?.serviceLocation
if (!location?.id) return null
return { id: location.id, name: location.name || '已选服务站点' }
} catch {
return null
}
},
getMyAppointments() {
return get('/service-appointments')
},
getAppointmentDetail(id: string) {
return get(`/service-appointments/${encodeURIComponent(id)}`)
},
createAppointment(payload: Record<string, any>) {
return post('/service-appointments', {
serviceLocationId: payload.serviceLocationId,
serviceType: payload.serviceType,
preferredDate: payload.preferredDate,
preferredTimeSlot: payload.preferredTimeSlot,
userNote: payload.userNote,
})
},
cancelAppointment(id: string, reason?: string) {
return post(`/service-appointments/${encodeURIComponent(id)}/cancel`, { reason: reason || '' })
},
}
/** 善缘服务站 */
export const serviceStationService = {
async getStation() {
const home: any = await get('/home')
return home?.serviceLocation || null
},
async getVisibleAnnouncements(locationId: string) {
if (!locationId) return []
try {
const list = await get(`/service-locations/${encodeURIComponent(locationId)}/announcements`)
return Array.isArray(list) ? list : []
} catch {
return []
}
},
}
/** 专项公益服务项目 */
export const serviceProgramService = {
getPrograms(params: Record<string, any> = {}) {
return get('/service-programs', params)
},
getProgramDetail(id: string) {
return get(`/service-programs/${encodeURIComponent(id)}`)
},
applyToProgram(id: string) {
return post(`/service-programs/${encodeURIComponent(id)}/apply`, {}, { idempotencyKey: idempotencyKey('program-apply', id) })
},
getMyApplications() {
return get('/service-programs/my')
},
}
/** 公益内容(婚恋课堂 / 反诈) */
export const contentService = {
getCategories() {
return get('/content/categories')
},
getArticles(params: Record<string, any> = {}) {
return get('/content/articles', params)
},
getArticleDetail(id: string) {
return get(`/content/articles/${encodeURIComponent(id)}`)
},
}
/** 婚恋咨询 / 婚前辅导 */
export const counselingService = {
intro() {
return get('/counseling/services/intro')
},
apply(payload: Record<string, any>) {
return post('/counseling/services', payload, { idempotencyKey: idempotencyKey('counseling-apply', payload.serviceType) })
},
myServices(limit = 20) {
return get('/counseling/services', { limit })
},
detail(id: string) {
return get(`/counseling/services/${encodeURIComponent(id)}`)
},
cancel(id: string, reason?: string) {
return post(`/counseling/services/${encodeURIComponent(id)}/cancel`, { reason: reason || '' })
},
confirmCompanion(id: string) {
return post(`/counseling/services/${encodeURIComponent(id)}/companion-confirm`, {}, {
idempotencyKey: idempotencyKey('companion-confirm', id),
})
},
}