333 lines
9.4 KiB
TypeScript
333 lines
9.4 KiB
TypeScript
import type {
|
|
CleaningSettlement,
|
|
CleaningSettlementDetail,
|
|
CleaningStatistics,
|
|
CleaningTask,
|
|
CleaningTaskEvent,
|
|
CleaningTaskMember,
|
|
ManagedUser,
|
|
PageResult,
|
|
PayoutStateFilter,
|
|
StaffRole,
|
|
SettlementStatus,
|
|
TaskStatus,
|
|
TransferMode,
|
|
UserStatus,
|
|
WechatTransferPreflight
|
|
} from './types';
|
|
|
|
const API_BASE = (import.meta.env.VITE_ADMIN_API_BASE_URL || '/admin-api').replace(/\/$/, '');
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public readonly code: string,
|
|
message = code,
|
|
public readonly traceId = ''
|
|
) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
export interface ApiSession {
|
|
token: string;
|
|
}
|
|
|
|
async function request<T>(
|
|
session: ApiSession,
|
|
path: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
if (!session.token.trim()) {
|
|
throw new ApiError('AUTH_TOKEN_REQUIRED', '需要先填入后台访问令牌');
|
|
}
|
|
const headers = new Headers(options.headers);
|
|
headers.set('authorization', `Bearer ${session.token.trim()}`);
|
|
if (options.body && !headers.has('content-type')) {
|
|
headers.set('content-type', 'application/json');
|
|
}
|
|
const response = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok || payload.code !== 0) {
|
|
throw new ApiError(
|
|
String(payload.code || `HTTP_${response.status}`),
|
|
String(payload.message || '请求失败'),
|
|
String(payload.traceId || '')
|
|
);
|
|
}
|
|
return payload.data as T;
|
|
}
|
|
|
|
export function listCleaningTasks(
|
|
session: ApiSession,
|
|
input: { page: number; pageSize: number; status?: TaskStatus; storeId?: string; cleanerUserId?: string }
|
|
) {
|
|
const params = new URLSearchParams({
|
|
page: String(input.page),
|
|
pageSize: String(input.pageSize)
|
|
});
|
|
if (input.status) params.set('status', input.status);
|
|
if (input.storeId) params.set('storeId', input.storeId);
|
|
if (input.cleanerUserId) params.set('cleanerUserId', input.cleanerUserId);
|
|
return request<PageResult<CleaningTask>>(session, `/cleaning/tasks?${params}`);
|
|
}
|
|
|
|
export function getCleaningStatistics(
|
|
session: ApiSession,
|
|
input: { from?: string; to?: string; storeId?: string; cleanerUserId?: string } = {}
|
|
) {
|
|
const params = new URLSearchParams();
|
|
if (input.from) params.set('from', input.from);
|
|
if (input.to) params.set('to', input.to);
|
|
if (input.storeId) params.set('storeId', input.storeId);
|
|
if (input.cleanerUserId) params.set('cleanerUserId', input.cleanerUserId);
|
|
const query = params.toString();
|
|
return request<CleaningStatistics>(session, `/cleaning/statistics${query ? `?${query}` : ''}`);
|
|
}
|
|
|
|
export function listStaffUsers(
|
|
session: ApiSession,
|
|
input: {
|
|
page: number;
|
|
pageSize: number;
|
|
role?: StaffRole;
|
|
status?: UserStatus;
|
|
search?: string;
|
|
}
|
|
) {
|
|
const params = new URLSearchParams({
|
|
page: String(input.page),
|
|
pageSize: String(input.pageSize)
|
|
});
|
|
if (input.role) params.set('role', input.role);
|
|
if (input.status) params.set('status', input.status);
|
|
if (input.search) params.set('search', input.search);
|
|
return request<{ items: ManagedUser[]; total: number }>(session, `/users?${params}`);
|
|
}
|
|
|
|
export function createStaffUser(
|
|
session: ApiSession,
|
|
input: { nickname: string; phone: string; note?: string; roles: StaffRole[]; storeIds: string[] }
|
|
) {
|
|
return request<{ userId: string }>(session, '/staff', {
|
|
method: 'POST',
|
|
body: JSON.stringify(input)
|
|
});
|
|
}
|
|
|
|
export function updateStaffUser(
|
|
session: ApiSession,
|
|
userId: string,
|
|
input: Partial<{
|
|
nickname: string;
|
|
phone: string;
|
|
note: string;
|
|
status: UserStatus;
|
|
roles: StaffRole[];
|
|
storeIds: string[];
|
|
}>
|
|
) {
|
|
return request<{ userId: string }>(session, `/users/${encodeURIComponent(userId)}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(input)
|
|
});
|
|
}
|
|
|
|
export function resetStaffSessions(session: ApiSession, userId: string) {
|
|
return request<{ userId: string; revokedSessions: number }>(
|
|
session,
|
|
`/users/${encodeURIComponent(userId)}/reset-sessions`,
|
|
{ method: 'POST' }
|
|
);
|
|
}
|
|
|
|
export function assignCleaningTask(
|
|
session: ApiSession,
|
|
taskId: string,
|
|
input: { cleanerUserId: string; note?: string }
|
|
) {
|
|
return request<CleaningTask>(session, `/cleaning/tasks/${encodeURIComponent(taskId)}/assign`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(input)
|
|
});
|
|
}
|
|
|
|
export function completeCleaningTask(session: ApiSession, taskId: string, note?: string) {
|
|
return request<CleaningTask>(session, `/cleaning/tasks/${encodeURIComponent(taskId)}/complete`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ note })
|
|
});
|
|
}
|
|
|
|
export function rejectCleaningTask(session: ApiSession, taskId: string, reason: string) {
|
|
return request<CleaningTask>(session, `/cleaning/tasks/${encodeURIComponent(taskId)}/reject`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ reason })
|
|
});
|
|
}
|
|
|
|
export function listCleaningTaskMembers(session: ApiSession, taskId: string) {
|
|
return request<CleaningTaskMember[]>(
|
|
session,
|
|
`/cleaning/tasks/${encodeURIComponent(taskId)}/members`
|
|
);
|
|
}
|
|
|
|
export function listCleaningTaskEvents(session: ApiSession, taskId: string) {
|
|
return request<CleaningTaskEvent[]>(
|
|
session,
|
|
`/cleaning/tasks/${encodeURIComponent(taskId)}/events`
|
|
);
|
|
}
|
|
|
|
export function addCleaningTaskMember(
|
|
session: ApiSession,
|
|
taskId: string,
|
|
input: { cleanerUserId: string; rewardCents: number; note?: string }
|
|
) {
|
|
return request<CleaningTaskMember[]>(
|
|
session,
|
|
`/cleaning/tasks/${encodeURIComponent(taskId)}/members`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(input)
|
|
}
|
|
);
|
|
}
|
|
|
|
export function removeCleaningTaskMember(
|
|
session: ApiSession,
|
|
taskId: string,
|
|
cleanerUserId: string,
|
|
note?: string
|
|
) {
|
|
return request<CleaningTaskMember[]>(
|
|
session,
|
|
`/cleaning/tasks/${encodeURIComponent(taskId)}/members/${encodeURIComponent(cleanerUserId)}/remove`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify({ note })
|
|
}
|
|
);
|
|
}
|
|
|
|
export function reclaimCleaningTimeouts(
|
|
session: ApiSession,
|
|
input: { olderThanMinutes: number; limit: number }
|
|
) {
|
|
return request<{ reclaimed: number; taskIds: string[] }>(session, '/cleaning/reclaim-timeouts', {
|
|
method: 'POST',
|
|
body: JSON.stringify(input)
|
|
});
|
|
}
|
|
|
|
export function listCleaningSettlements(
|
|
session: ApiSession,
|
|
input: {
|
|
page: number;
|
|
pageSize: number;
|
|
status?: SettlementStatus;
|
|
payoutState?: PayoutStateFilter;
|
|
storeId?: string;
|
|
cleanerUserId?: string;
|
|
}
|
|
) {
|
|
const params = new URLSearchParams({
|
|
page: String(input.page),
|
|
pageSize: String(input.pageSize)
|
|
});
|
|
if (input.status) params.set('status', input.status);
|
|
if (input.payoutState) params.set('payoutState', input.payoutState);
|
|
if (input.storeId) params.set('storeId', input.storeId);
|
|
if (input.cleanerUserId) params.set('cleanerUserId', input.cleanerUserId);
|
|
return request<PageResult<CleaningSettlement>>(session, `/cleaning/settlements?${params}`);
|
|
}
|
|
|
|
export function listCleaningSettlementCandidates(
|
|
session: ApiSession,
|
|
input: { page: number; pageSize: number; storeId?: string; cleanerUserId?: string }
|
|
) {
|
|
const params = new URLSearchParams({
|
|
page: String(input.page),
|
|
pageSize: String(input.pageSize)
|
|
});
|
|
if (input.storeId) params.set('storeId', input.storeId);
|
|
if (input.cleanerUserId) params.set('cleanerUserId', input.cleanerUserId);
|
|
return request<PageResult<CleaningTask>>(session, `/cleaning/settlement-candidates?${params}`);
|
|
}
|
|
|
|
export function generateCleaningSettlement(
|
|
session: ApiSession,
|
|
input: { cleanerUserId: string; storeId?: string; note?: string }
|
|
) {
|
|
return request<CleaningSettlement>(session, '/cleaning/settlements', {
|
|
method: 'POST',
|
|
body: JSON.stringify(input)
|
|
});
|
|
}
|
|
|
|
export function getCleaningSettlementDetail(session: ApiSession, settlementId: string) {
|
|
return request<CleaningSettlementDetail>(
|
|
session,
|
|
`/cleaning/settlements/${encodeURIComponent(settlementId)}`
|
|
);
|
|
}
|
|
|
|
export function confirmCleaningSettlement(session: ApiSession, settlementId: string, note?: string) {
|
|
return request<CleaningSettlement>(
|
|
session,
|
|
`/cleaning/settlements/${encodeURIComponent(settlementId)}/confirm`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify({ note })
|
|
}
|
|
);
|
|
}
|
|
|
|
export function executeWechatTransfer(
|
|
session: ApiSession,
|
|
settlementId: string,
|
|
input: { mode: TransferMode; note?: string }
|
|
) {
|
|
return request<{ settlement: CleaningSettlement; transferState: string; idempotent: boolean }>(
|
|
session,
|
|
`/cleaning/settlements/${encodeURIComponent(settlementId)}/wechat-transfer`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(input)
|
|
}
|
|
);
|
|
}
|
|
|
|
export function preflightWechatTransfer(session: ApiSession, settlementId: string) {
|
|
return request<WechatTransferPreflight>(
|
|
session,
|
|
`/cleaning/settlements/${encodeURIComponent(settlementId)}/wechat-transfer/preflight`
|
|
);
|
|
}
|
|
|
|
export function syncWechatTransfer(session: ApiSession, settlementId: string, note?: string) {
|
|
return request<{ settlement: CleaningSettlement; transferState: string; idempotent: boolean }>(
|
|
session,
|
|
`/cleaning/settlements/${encodeURIComponent(settlementId)}/wechat-transfer/sync`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify({ note })
|
|
}
|
|
);
|
|
}
|
|
|
|
export function recordPayoutFailure(
|
|
session: ApiSession,
|
|
settlementId: string,
|
|
input: { error: string; payoutChannel?: string; payoutReference?: string; note?: string }
|
|
) {
|
|
return request<CleaningSettlement>(
|
|
session,
|
|
`/cleaning/settlements/${encodeURIComponent(settlementId)}/payout-failure`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(input)
|
|
}
|
|
);
|
|
}
|