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
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:
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}`),
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -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')
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user