新增打手撤单审核
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
-- 打手撤单申请:提交后由客服审核,审核通过才执行撤单与处罚。
|
||||||
|
|
||||||
|
ALTER TABLE worker_users
|
||||||
|
ADD COLUMN IF NOT EXISTS level_penalty_accepted_count INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS worker_order_cancel_requests (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
work_order_id BIGINT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
|
||||||
|
work_order_share_id BIGINT REFERENCES work_order_shares(id) ON DELETE SET NULL,
|
||||||
|
worker_id BIGINT NOT NULL REFERENCES worker_users(id) ON DELETE CASCADE,
|
||||||
|
reason TEXT NOT NULL DEFAULT '',
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
review_note TEXT NOT NULL DEFAULT '',
|
||||||
|
reviewed_by TEXT NOT NULL DEFAULT '',
|
||||||
|
level_before_id BIGINT REFERENCES worker_levels(id) ON DELETE SET NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
|
reviewed_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_worker_cancel_requests_pending_target
|
||||||
|
ON worker_order_cancel_requests (work_order_id, worker_id, COALESCE(work_order_share_id, 0))
|
||||||
|
WHERE status = 'pending';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_worker_cancel_requests_worker_created
|
||||||
|
ON worker_order_cancel_requests (worker_id, created_at DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_worker_cancel_requests_status_created
|
||||||
|
ON worker_order_cancel_requests (status, created_at DESC);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN worker_users.level_penalty_accepted_count IS '撤单处罚扣减的有效验收次数,不删除真实历史订单';
|
||||||
|
COMMENT ON TABLE worker_order_cancel_requests IS '打手发起、客服审核的撤单申请';
|
||||||
@@ -30,6 +30,7 @@ export type WorkerUserRow = {
|
|||||||
level_key?: string
|
level_key?: string
|
||||||
level_name?: string
|
level_name?: string
|
||||||
level_permission_json?: string | Record<string, unknown>
|
level_permission_json?: string | Record<string, unknown>
|
||||||
|
level_penalty_accepted_count?: number
|
||||||
available_amount?: number
|
available_amount?: number
|
||||||
frozen_deposit_amount?: number
|
frozen_deposit_amount?: number
|
||||||
pending_unfreeze_amount?: number
|
pending_unfreeze_amount?: number
|
||||||
@@ -66,6 +67,26 @@ export type WorkerWalletRow = {
|
|||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkerCancelRequestRow = {
|
||||||
|
id: number
|
||||||
|
work_order_id: number
|
||||||
|
work_order_share_id: number | null
|
||||||
|
worker_id: number
|
||||||
|
reason: string
|
||||||
|
status: string
|
||||||
|
review_note: string
|
||||||
|
reviewed_by: string
|
||||||
|
level_before_id: number | null
|
||||||
|
created_at: string
|
||||||
|
reviewed_at: string | null
|
||||||
|
updated_at: string
|
||||||
|
work_order_no?: string
|
||||||
|
product_name?: string
|
||||||
|
worker_username?: string
|
||||||
|
worker_display_name?: string
|
||||||
|
share_quantity?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkerDepositUnfreezeRow = {
|
export type WorkerDepositUnfreezeRow = {
|
||||||
id: number
|
id: number
|
||||||
worker_id: number
|
worker_id: number
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type {
|
|||||||
ProductRuleListInput,
|
ProductRuleListInput,
|
||||||
WorkCategoryRow,
|
WorkCategoryRow,
|
||||||
WorkerDepositUnfreezeRow,
|
WorkerDepositUnfreezeRow,
|
||||||
|
WorkerCancelRequestRow,
|
||||||
WorkOrderRow,
|
WorkOrderRow,
|
||||||
WorkOrderShareRow,
|
WorkOrderShareRow,
|
||||||
WorkOrderStatistics,
|
WorkOrderStatistics,
|
||||||
@@ -59,6 +60,20 @@ const WORK_ORDER_SHARE_SELECT = `
|
|||||||
LEFT JOIN worker_users wu ON wu.id = wos.worker_id
|
LEFT JOIN worker_users wu ON wu.id = wos.worker_id
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const WORKER_CANCEL_REQUEST_SELECT = `
|
||||||
|
SELECT
|
||||||
|
cr.*,
|
||||||
|
wo.work_order_no,
|
||||||
|
wo.product_name,
|
||||||
|
wu.username AS worker_username,
|
||||||
|
wu.display_name AS worker_display_name,
|
||||||
|
wos.quantity AS share_quantity
|
||||||
|
FROM worker_order_cancel_requests cr
|
||||||
|
INNER JOIN work_orders wo ON wo.id = cr.work_order_id
|
||||||
|
INNER JOIN worker_users wu ON wu.id = cr.worker_id
|
||||||
|
LEFT JOIN work_order_shares wos ON wos.id = cr.work_order_share_id
|
||||||
|
`
|
||||||
|
|
||||||
type WorkerOrderOverviewRow = {
|
type WorkerOrderOverviewRow = {
|
||||||
status: string
|
status: string
|
||||||
order_count: number
|
order_count: number
|
||||||
@@ -1707,6 +1722,413 @@ export async function cancelWorkerWorkOrder(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 打手提交撤单申请;待审核期间订单与押金均保持原状。 */
|
||||||
|
export async function createWorkerCancelRequest(input: {
|
||||||
|
workOrderId: number
|
||||||
|
workerId: number
|
||||||
|
reason: string
|
||||||
|
now: string
|
||||||
|
}): Promise<{
|
||||||
|
request: WorkerCancelRequestRow | null
|
||||||
|
dailyUsed: number
|
||||||
|
failureReason:
|
||||||
|
| 'work_order_not_cancellable'
|
||||||
|
| 'work_order_owner_required'
|
||||||
|
| 'share_not_cancellable'
|
||||||
|
| 'request_pending'
|
||||||
|
| 'daily_limit_reached'
|
||||||
|
| null
|
||||||
|
}> {
|
||||||
|
return withTransaction(async (client) => {
|
||||||
|
const orderResult = await client.query<WorkOrderRow>(
|
||||||
|
'SELECT * FROM work_orders WHERE id = $1 FOR UPDATE',
|
||||||
|
[input.workOrderId],
|
||||||
|
)
|
||||||
|
const workOrder = orderResult.rows[0] || null
|
||||||
|
if (!workOrder) {
|
||||||
|
return { request: null, dailyUsed: 0, failureReason: 'work_order_not_cancellable' }
|
||||||
|
}
|
||||||
|
|
||||||
|
let shareId: number | null = null
|
||||||
|
if (workOrder.sharing_enabled === true) {
|
||||||
|
const shareResult = await client.query<WorkOrderShareRow>(
|
||||||
|
`SELECT * FROM work_order_shares
|
||||||
|
WHERE work_order_id = $1 AND worker_id = $2
|
||||||
|
FOR UPDATE`,
|
||||||
|
[input.workOrderId, input.workerId],
|
||||||
|
)
|
||||||
|
const share = shareResult.rows[0] || null
|
||||||
|
if (!share || !resolveWorkOrderShareCancellationStatus(workOrder.status, share.status)) {
|
||||||
|
return { request: null, dailyUsed: 0, failureReason: 'share_not_cancellable' }
|
||||||
|
}
|
||||||
|
shareId = Number(share.id)
|
||||||
|
} else if (
|
||||||
|
workOrder.status !== 'in_progress' ||
|
||||||
|
Number(workOrder.assigned_worker_id || 0) !== input.workerId
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
request: null,
|
||||||
|
dailyUsed: 0,
|
||||||
|
failureReason: workOrder.assigned_worker_id
|
||||||
|
? 'work_order_not_cancellable'
|
||||||
|
: 'work_order_owner_required',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dailyResult = await client.query<{ total: number }>(
|
||||||
|
`SELECT COUNT(*)::int AS total
|
||||||
|
FROM worker_order_cancel_requests
|
||||||
|
WHERE worker_id = $1
|
||||||
|
AND status IN ('pending', 'approved')
|
||||||
|
AND created_at >= date_trunc('day', $2::timestamptz AT TIME ZONE 'Asia/Shanghai') AT TIME ZONE 'Asia/Shanghai'`,
|
||||||
|
[input.workerId, input.now],
|
||||||
|
)
|
||||||
|
const dailyUsed = Number(dailyResult.rows[0]?.total || 0)
|
||||||
|
if (dailyUsed >= 5) {
|
||||||
|
return { request: null, dailyUsed, failureReason: 'daily_limit_reached' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingResult = await client.query<{ id: number }>(
|
||||||
|
`SELECT id FROM worker_order_cancel_requests
|
||||||
|
WHERE work_order_id = $1
|
||||||
|
AND worker_id = $2
|
||||||
|
AND COALESCE(work_order_share_id, 0) = $3
|
||||||
|
AND status = 'pending'
|
||||||
|
LIMIT 1`,
|
||||||
|
[input.workOrderId, input.workerId, shareId || 0],
|
||||||
|
)
|
||||||
|
if (pendingResult.rows[0]) {
|
||||||
|
return { request: null, dailyUsed, failureReason: 'request_pending' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await client.query<{ id: number }>(
|
||||||
|
`INSERT INTO worker_order_cancel_requests (
|
||||||
|
work_order_id, work_order_share_id, worker_id, reason, status, created_at, updated_at
|
||||||
|
) VALUES ($1, $2, $3, $4, 'pending', $5, $5)
|
||||||
|
RETURNING id`,
|
||||||
|
[input.workOrderId, shareId, input.workerId, input.reason, input.now],
|
||||||
|
)
|
||||||
|
const requestId = Number(created.rows[0]?.id || 0)
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
actorType: 'worker',
|
||||||
|
actorId: String(input.workerId),
|
||||||
|
eventType: 'cancel_request_submitted',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: workOrder.status,
|
||||||
|
payloadJson: JSON.stringify({ requestId, shareId, reason: input.reason }),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
request: await getWorkerCancelRequestByIdWithClient(client, requestId),
|
||||||
|
dailyUsed: dailyUsed + 1,
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listWorkerCancelRequests(
|
||||||
|
workerId: number | string,
|
||||||
|
): Promise<WorkerCancelRequestRow[]> {
|
||||||
|
const result = await query<WorkerCancelRequestRow>(
|
||||||
|
`${WORKER_CANCEL_REQUEST_SELECT}
|
||||||
|
WHERE cr.worker_id = $1
|
||||||
|
ORDER BY cr.id DESC
|
||||||
|
LIMIT 100`,
|
||||||
|
[Number(workerId)],
|
||||||
|
)
|
||||||
|
return result.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAdminWorkerCancelRequests(
|
||||||
|
status = 'pending',
|
||||||
|
): Promise<WorkerCancelRequestRow[]> {
|
||||||
|
const result = await query<WorkerCancelRequestRow>(
|
||||||
|
`${WORKER_CANCEL_REQUEST_SELECT}
|
||||||
|
WHERE ($1 = '' OR cr.status = $1)
|
||||||
|
ORDER BY cr.id DESC
|
||||||
|
LIMIT 200`,
|
||||||
|
[status],
|
||||||
|
)
|
||||||
|
return result.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorkerCancelRequestRisk(workerId: number | string, now: string) {
|
||||||
|
const result = await query<{ daily_used: number; approved_total: number }>(
|
||||||
|
`SELECT
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE status IN ('pending', 'approved')
|
||||||
|
AND created_at >= date_trunc('day', $2::timestamptz AT TIME ZONE 'Asia/Shanghai') AT TIME ZONE 'Asia/Shanghai'
|
||||||
|
)::int AS daily_used,
|
||||||
|
COUNT(*) FILTER (WHERE status = 'approved')::int AS approved_total
|
||||||
|
FROM worker_order_cancel_requests
|
||||||
|
WHERE worker_id = $1`,
|
||||||
|
[Number(workerId), now],
|
||||||
|
)
|
||||||
|
const row = result.rows[0]
|
||||||
|
const approvedTotal = Number(row?.approved_total || 0)
|
||||||
|
const nextPenaltyAt = Math.ceil((approvedTotal + 1) / 10) * 10
|
||||||
|
return {
|
||||||
|
dailyUsed: Number(row?.daily_used || 0),
|
||||||
|
dailyLimit: 5,
|
||||||
|
approvedTotal,
|
||||||
|
nextPenaltyAt,
|
||||||
|
remainingToPenalty: Math.max(0, nextPenaltyAt - approvedTotal),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 客服审核通过时在同一事务内撤单、退押金并执行等级处罚。 */
|
||||||
|
export async function reviewWorkerCancelRequest(input: {
|
||||||
|
requestId: number
|
||||||
|
approved: boolean
|
||||||
|
reviewNote: string
|
||||||
|
actorName: string
|
||||||
|
now: string
|
||||||
|
}): Promise<{
|
||||||
|
request: WorkerCancelRequestRow | null
|
||||||
|
order: WorkOrderRow | null
|
||||||
|
releasedDepositAmount: number
|
||||||
|
levelChanged: boolean
|
||||||
|
accountDisabled: boolean
|
||||||
|
failureReason: 'request_not_found' | 'request_not_pending' | 'target_state_changed' | null
|
||||||
|
}> {
|
||||||
|
return withTransaction(async (client) => {
|
||||||
|
const requestResult = await client.query<WorkerCancelRequestRow>(
|
||||||
|
'SELECT * FROM worker_order_cancel_requests WHERE id = $1 FOR UPDATE',
|
||||||
|
[input.requestId],
|
||||||
|
)
|
||||||
|
const request = requestResult.rows[0] || null
|
||||||
|
if (!request) {
|
||||||
|
return {
|
||||||
|
request: null,
|
||||||
|
order: null,
|
||||||
|
releasedDepositAmount: 0,
|
||||||
|
levelChanged: false,
|
||||||
|
accountDisabled: false,
|
||||||
|
failureReason: 'request_not_found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (request.status !== 'pending') {
|
||||||
|
return {
|
||||||
|
request: await getWorkerCancelRequestByIdWithClient(client, request.id),
|
||||||
|
order: null,
|
||||||
|
releasedDepositAmount: 0,
|
||||||
|
levelChanged: false,
|
||||||
|
accountDisabled: false,
|
||||||
|
failureReason: 'request_not_pending',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workOrderResult = await client.query<WorkOrderRow>(
|
||||||
|
'SELECT * FROM work_orders WHERE id = $1 FOR UPDATE',
|
||||||
|
[request.work_order_id],
|
||||||
|
)
|
||||||
|
const workOrder = workOrderResult.rows[0] || null
|
||||||
|
if (!workOrder) {
|
||||||
|
return {
|
||||||
|
request: null,
|
||||||
|
order: null,
|
||||||
|
releasedDepositAmount: 0,
|
||||||
|
levelChanged: false,
|
||||||
|
accountDisabled: false,
|
||||||
|
failureReason: 'target_state_changed',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.approved) {
|
||||||
|
await client.query(
|
||||||
|
`UPDATE worker_order_cancel_requests
|
||||||
|
SET status = 'rejected', review_note = $1, reviewed_by = $2, reviewed_at = $3, updated_at = $3
|
||||||
|
WHERE id = $4`,
|
||||||
|
[input.reviewNote, input.actorName, input.now, request.id],
|
||||||
|
)
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: input.actorName,
|
||||||
|
eventType: 'cancel_request_rejected',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: workOrder.status,
|
||||||
|
payloadJson: JSON.stringify({ requestId: request.id, note: input.reviewNote }),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
request: await getWorkerCancelRequestByIdWithClient(client, request.id),
|
||||||
|
order: await getWorkOrderByIdWithClient(client, workOrder.id),
|
||||||
|
releasedDepositAmount: 0,
|
||||||
|
levelChanged: false,
|
||||||
|
accountDisabled: false,
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let releasedDepositAmount = 0
|
||||||
|
if (request.work_order_share_id) {
|
||||||
|
const shareResult = await client.query<WorkOrderShareRow>(
|
||||||
|
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
|
||||||
|
[request.work_order_share_id, request.work_order_id],
|
||||||
|
)
|
||||||
|
const share = shareResult.rows[0] || null
|
||||||
|
if (
|
||||||
|
!share ||
|
||||||
|
Number(share.worker_id) !== Number(request.worker_id) ||
|
||||||
|
!resolveWorkOrderShareCancellationStatus(workOrder.status, share.status)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
request: null,
|
||||||
|
order: null,
|
||||||
|
releasedDepositAmount: 0,
|
||||||
|
levelChanged: false,
|
||||||
|
accountDisabled: false,
|
||||||
|
failureReason: 'target_state_changed',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await ensureWorkerWalletWithClient(client, Number(request.worker_id), input.now)
|
||||||
|
const wallet = await getWorkerWalletWithClient(client, Number(request.worker_id))
|
||||||
|
releasedDepositAmount = Math.min(
|
||||||
|
Number(share.share_deposit || 0),
|
||||||
|
Number(wallet?.frozen_deposit_amount || 0),
|
||||||
|
)
|
||||||
|
if (releasedDepositAmount > 0) {
|
||||||
|
const nextAvailable = Number(wallet?.available_amount || 0) + releasedDepositAmount
|
||||||
|
const nextFrozen = Math.max(
|
||||||
|
0,
|
||||||
|
Number(wallet?.frozen_deposit_amount || 0) - releasedDepositAmount,
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`UPDATE worker_wallets
|
||||||
|
SET available_amount = $1, frozen_deposit_amount = $2, updated_at = $3
|
||||||
|
WHERE worker_id = $4`,
|
||||||
|
[nextAvailable, nextFrozen, input.now, request.worker_id],
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO worker_wallet_ledgers (
|
||||||
|
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||||
|
related_work_order_id, note, payload_json, created_at
|
||||||
|
) VALUES ($1, 'deposit_release', $2, $3, $4, $5, '撤单申请审核通过退还拼单押金', $6::jsonb, $7)`,
|
||||||
|
[
|
||||||
|
request.worker_id,
|
||||||
|
releasedDepositAmount,
|
||||||
|
nextAvailable,
|
||||||
|
nextFrozen,
|
||||||
|
request.work_order_id,
|
||||||
|
JSON.stringify({ requestId: request.id, shareId: share.id, reason: request.reason }),
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await client.query(
|
||||||
|
`UPDATE work_order_shares SET status = 'cancelled', updated_at = $1 WHERE id = $2`,
|
||||||
|
[input.now, share.id],
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`UPDATE work_orders
|
||||||
|
SET status = 'open', submitted_at = NULL, published_at = COALESCE(published_at, $1), updated_at = $1
|
||||||
|
WHERE id = $2`,
|
||||||
|
[input.now, workOrder.id],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
workOrder.status !== 'in_progress' ||
|
||||||
|
Number(workOrder.assigned_worker_id || 0) !== Number(request.worker_id)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
request: null,
|
||||||
|
order: null,
|
||||||
|
releasedDepositAmount: 0,
|
||||||
|
levelChanged: false,
|
||||||
|
accountDisabled: false,
|
||||||
|
failureReason: 'target_state_changed',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await ensureWorkerWalletWithClient(client, Number(request.worker_id), input.now)
|
||||||
|
const wallet = await getWorkerWalletWithClient(client, Number(request.worker_id))
|
||||||
|
releasedDepositAmount = Math.min(
|
||||||
|
await getOutstandingDepositAmountWithClient(
|
||||||
|
client,
|
||||||
|
Number(request.worker_id),
|
||||||
|
workOrder.id,
|
||||||
|
),
|
||||||
|
Number(wallet?.frozen_deposit_amount || 0),
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`UPDATE work_orders
|
||||||
|
SET status = 'open', assigned_worker_id = NULL, assigned_at = NULL, deadline_at = NULL,
|
||||||
|
published_at = $1, updated_at = $1
|
||||||
|
WHERE id = $2`,
|
||||||
|
[input.now, workOrder.id],
|
||||||
|
)
|
||||||
|
if (releasedDepositAmount > 0) {
|
||||||
|
const nextAvailable = Number(wallet?.available_amount || 0) + releasedDepositAmount
|
||||||
|
const nextFrozen = Math.max(
|
||||||
|
0,
|
||||||
|
Number(wallet?.frozen_deposit_amount || 0) - releasedDepositAmount,
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`UPDATE worker_wallets
|
||||||
|
SET available_amount = $1, frozen_deposit_amount = $2, updated_at = $3
|
||||||
|
WHERE worker_id = $4`,
|
||||||
|
[nextAvailable, nextFrozen, input.now, request.worker_id],
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO worker_wallet_ledgers (
|
||||||
|
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||||
|
related_work_order_id, note, payload_json, created_at
|
||||||
|
) VALUES ($1, 'deposit_release', $2, $3, $4, $5, '撤单申请审核通过退还押金', $6::jsonb, $7)`,
|
||||||
|
[
|
||||||
|
request.worker_id,
|
||||||
|
releasedDepositAmount,
|
||||||
|
nextAvailable,
|
||||||
|
nextFrozen,
|
||||||
|
request.work_order_id,
|
||||||
|
JSON.stringify({ requestId: request.id, reason: request.reason }),
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const penalty = await applyWorkerCancelRequestPenaltyWithClient(client, {
|
||||||
|
workerId: Number(request.worker_id),
|
||||||
|
requestId: Number(request.id),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
await client.query(
|
||||||
|
`UPDATE worker_order_cancel_requests
|
||||||
|
SET status = 'approved', review_note = $1, reviewed_by = $2, reviewed_at = $3,
|
||||||
|
level_before_id = $4, updated_at = $3
|
||||||
|
WHERE id = $5`,
|
||||||
|
[input.reviewNote, input.actorName, input.now, penalty.levelBeforeId, request.id],
|
||||||
|
)
|
||||||
|
const updatedOrder = await getWorkOrderByIdWithClient(client, workOrder.id)
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: input.actorName,
|
||||||
|
eventType: 'cancel_request_approved',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: updatedOrder?.status || workOrder.status,
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
requestId: request.id,
|
||||||
|
shareId: request.work_order_share_id,
|
||||||
|
releasedDepositAmount,
|
||||||
|
levelChanged: penalty.levelChanged,
|
||||||
|
accountDisabled: penalty.accountDisabled,
|
||||||
|
note: input.reviewNote,
|
||||||
|
}),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
request: await getWorkerCancelRequestByIdWithClient(client, request.id),
|
||||||
|
order: updatedOrder,
|
||||||
|
releasedDepositAmount,
|
||||||
|
levelChanged: penalty.levelChanged,
|
||||||
|
accountDisabled: penalty.accountDisabled,
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function unassignWorkOrder(input: {
|
export async function unassignWorkOrder(input: {
|
||||||
workOrderId: number
|
workOrderId: number
|
||||||
now: string
|
now: string
|
||||||
@@ -3349,6 +3771,171 @@ async function createWorkOrderEventWithClient(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getWorkerCancelRequestByIdWithClient(
|
||||||
|
client: PoolClient,
|
||||||
|
requestId: number | string,
|
||||||
|
): Promise<WorkerCancelRequestRow | null> {
|
||||||
|
const result = await client.query<WorkerCancelRequestRow>(
|
||||||
|
`${WORKER_CANCEL_REQUEST_SELECT} WHERE cr.id = $1 LIMIT 1`,
|
||||||
|
[Number(requestId)],
|
||||||
|
)
|
||||||
|
return result.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function countWorkerAcceptedOrdersWithClient(
|
||||||
|
client: PoolClient,
|
||||||
|
workerId: number,
|
||||||
|
): Promise<number> {
|
||||||
|
const result = await client.query<{ total: number }>(
|
||||||
|
`SELECT COUNT(DISTINCT order_key)::int AS total
|
||||||
|
FROM (
|
||||||
|
SELECT COALESCE(
|
||||||
|
'order:' || NULLIF(wo.order_id::text, ''),
|
||||||
|
'platform_order:' || NULLIF(BTRIM(wo.platform_order_id), ''),
|
||||||
|
'work_order:' || wo.id::text
|
||||||
|
) AS order_key
|
||||||
|
FROM work_orders wo
|
||||||
|
WHERE wo.assigned_worker_id = $1 AND wo.status = 'accepted'
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
SELECT COALESCE(
|
||||||
|
'order:' || NULLIF(wo.order_id::text, ''),
|
||||||
|
'platform_order:' || NULLIF(BTRIM(wo.platform_order_id), ''),
|
||||||
|
'work_order:' || wo.id::text
|
||||||
|
) AS order_key
|
||||||
|
FROM work_order_shares wos
|
||||||
|
INNER JOIN work_orders wo ON wo.id = wos.work_order_id
|
||||||
|
WHERE wos.worker_id = $1 AND wos.status = 'accepted'
|
||||||
|
) AS completed_orders`,
|
||||||
|
[workerId],
|
||||||
|
)
|
||||||
|
return Number(result.rows[0]?.total || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将有效验收次数压到低一级门槛,保留真实订单与资金流水。 */
|
||||||
|
async function applyWorkerCancelRequestPenaltyWithClient(
|
||||||
|
client: PoolClient,
|
||||||
|
input: { workerId: number; requestId: number; now: string },
|
||||||
|
): Promise<{ levelBeforeId: number | null; levelChanged: boolean; accountDisabled: boolean }> {
|
||||||
|
const workerResult = await client.query<{
|
||||||
|
level_id: number | null
|
||||||
|
level_key: string
|
||||||
|
level_name: string
|
||||||
|
sort_order: number
|
||||||
|
level_penalty_accepted_count: number
|
||||||
|
}>(
|
||||||
|
`SELECT wu.level_id, wl.level_key, wl.name AS level_name, wl.sort_order,
|
||||||
|
wu.level_penalty_accepted_count
|
||||||
|
FROM worker_users wu
|
||||||
|
LEFT JOIN worker_levels wl ON wl.id = wu.level_id
|
||||||
|
WHERE wu.id = $1
|
||||||
|
FOR UPDATE OF wu`,
|
||||||
|
[input.workerId],
|
||||||
|
)
|
||||||
|
const worker = workerResult.rows[0]
|
||||||
|
const levelBeforeId = worker?.level_id ? Number(worker.level_id) : null
|
||||||
|
if (!worker || !levelBeforeId) {
|
||||||
|
return { levelBeforeId, levelChanged: false, accountDisabled: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const approvedResult = await client.query<{ total: number }>(
|
||||||
|
`SELECT COUNT(*)::int AS total
|
||||||
|
FROM worker_order_cancel_requests
|
||||||
|
WHERE worker_id = $1 AND status = 'approved'`,
|
||||||
|
[input.workerId],
|
||||||
|
)
|
||||||
|
const approvedTotal = Number(approvedResult.rows[0]?.total || 0) + 1
|
||||||
|
const isVip1 = String(worker.level_key || '').toLowerCase() === 'vip1'
|
||||||
|
|
||||||
|
if (isVip1) {
|
||||||
|
const vip1ApprovedResult = await client.query<{ total: number }>(
|
||||||
|
`SELECT COUNT(*)::int AS total
|
||||||
|
FROM worker_order_cancel_requests
|
||||||
|
WHERE worker_id = $1 AND status = 'approved' AND level_before_id = $2`,
|
||||||
|
[input.workerId, levelBeforeId],
|
||||||
|
)
|
||||||
|
const vip1ApprovedTotal = Number(vip1ApprovedResult.rows[0]?.total || 0) + 1
|
||||||
|
if (vip1ApprovedTotal >= 5) {
|
||||||
|
await client.query(
|
||||||
|
`UPDATE worker_users
|
||||||
|
SET status = 'disabled', session_version = session_version + 1, updated_at = $1
|
||||||
|
WHERE id = $2`,
|
||||||
|
[input.now, input.workerId],
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`UPDATE worker_sessions
|
||||||
|
SET status = 'revoked', revoked_at = $1, updated_at = $1
|
||||||
|
WHERE worker_id = $2 AND status = 'active'`,
|
||||||
|
[input.now, input.workerId],
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO worker_level_logs (
|
||||||
|
worker_id, from_level_id, to_level_id, reason, payload_json, created_at
|
||||||
|
) VALUES ($1, $2, $2, 'cancel_request_ban', $3::jsonb, $4)`,
|
||||||
|
[
|
||||||
|
input.workerId,
|
||||||
|
levelBeforeId,
|
||||||
|
JSON.stringify({ requestId: input.requestId, vip1ApprovedTotal, approvedTotal }),
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return { levelBeforeId, levelChanged: false, accountDisabled: true }
|
||||||
|
}
|
||||||
|
return { levelBeforeId, levelChanged: false, accountDisabled: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (approvedTotal % 10 !== 0) {
|
||||||
|
return { levelBeforeId, levelChanged: false, accountDisabled: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerLevelResult = await client.query<{
|
||||||
|
id: number
|
||||||
|
level_key: string
|
||||||
|
threshold: number
|
||||||
|
}>(
|
||||||
|
`SELECT id, level_key, COALESCE((permission_json->>'upgradeThreshold')::int, 0) AS threshold
|
||||||
|
FROM worker_levels
|
||||||
|
WHERE status = 'active' AND sort_order < $1
|
||||||
|
ORDER BY sort_order DESC, id DESC
|
||||||
|
LIMIT 1`,
|
||||||
|
[Number(worker.sort_order || 0)],
|
||||||
|
)
|
||||||
|
const lowerLevel = lowerLevelResult.rows[0]
|
||||||
|
if (!lowerLevel) {
|
||||||
|
return { levelBeforeId, levelChanged: false, accountDisabled: false }
|
||||||
|
}
|
||||||
|
const actualAcceptedCount = await countWorkerAcceptedOrdersWithClient(client, input.workerId)
|
||||||
|
const nextPenaltyAcceptedCount = Math.max(
|
||||||
|
Number(worker.level_penalty_accepted_count || 0),
|
||||||
|
actualAcceptedCount - Math.max(0, Number(lowerLevel.threshold || 0)),
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`UPDATE worker_users
|
||||||
|
SET level_id = $1, level_penalty_accepted_count = $2, updated_at = $3
|
||||||
|
WHERE id = $4`,
|
||||||
|
[lowerLevel.id, nextPenaltyAcceptedCount, input.now, input.workerId],
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO worker_level_logs (
|
||||||
|
worker_id, from_level_id, to_level_id, reason, payload_json, created_at
|
||||||
|
) VALUES ($1, $2, $3, 'cancel_request_penalty', $4::jsonb, $5)`,
|
||||||
|
[
|
||||||
|
input.workerId,
|
||||||
|
levelBeforeId,
|
||||||
|
lowerLevel.id,
|
||||||
|
JSON.stringify({
|
||||||
|
requestId: input.requestId,
|
||||||
|
approvedTotal,
|
||||||
|
actualAcceptedCount,
|
||||||
|
penaltyAcceptedCount: nextPenaltyAcceptedCount,
|
||||||
|
}),
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return { levelBeforeId, levelChanged: true, accountDisabled: false }
|
||||||
|
}
|
||||||
|
|
||||||
async function getOutstandingDepositAmountWithClient(
|
async function getOutstandingDepositAmountWithClient(
|
||||||
client: PoolClient,
|
client: PoolClient,
|
||||||
workerId: number,
|
workerId: number,
|
||||||
|
|||||||
@@ -1087,12 +1087,14 @@ export async function maybeUpgradeWorkerLevelWithClient(
|
|||||||
const current = await client.query<{
|
const current = await client.query<{
|
||||||
id: number
|
id: number
|
||||||
level_id: number | null
|
level_id: number | null
|
||||||
|
level_penalty_accepted_count: number
|
||||||
upgrade_threshold: number | null
|
upgrade_threshold: number | null
|
||||||
}>(
|
}>(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
wu.id,
|
wu.id,
|
||||||
wu.level_id,
|
wu.level_id,
|
||||||
|
wu.level_penalty_accepted_count,
|
||||||
COALESCE((wl.permission_json->>'upgradeThreshold')::int, 0) AS upgrade_threshold
|
COALESCE((wl.permission_json->>'upgradeThreshold')::int, 0) AS upgrade_threshold
|
||||||
FROM worker_users wu
|
FROM worker_users wu
|
||||||
LEFT JOIN worker_levels wl ON wl.id = wu.level_id
|
LEFT JOIN worker_levels wl ON wl.id = wu.level_id
|
||||||
@@ -1134,6 +1136,10 @@ export async function maybeUpgradeWorkerLevelWithClient(
|
|||||||
[workerId],
|
[workerId],
|
||||||
)
|
)
|
||||||
const acceptedCount = Number(acceptedResult.rows[0]?.total || 0)
|
const acceptedCount = Number(acceptedResult.rows[0]?.total || 0)
|
||||||
|
const effectiveAcceptedCount = Math.max(
|
||||||
|
0,
|
||||||
|
acceptedCount - Number(currentRow.level_penalty_accepted_count || 0),
|
||||||
|
)
|
||||||
|
|
||||||
const nextLevelResult = await client.query<{
|
const nextLevelResult = await client.query<{
|
||||||
id: number
|
id: number
|
||||||
@@ -1149,7 +1155,7 @@ export async function maybeUpgradeWorkerLevelWithClient(
|
|||||||
ORDER BY COALESCE((permission_json->>'upgradeThreshold')::int, 0) DESC, sort_order ASC
|
ORDER BY COALESCE((permission_json->>'upgradeThreshold')::int, 0) DESC, sort_order ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`,
|
`,
|
||||||
[acceptedCount],
|
[effectiveAcceptedCount],
|
||||||
)
|
)
|
||||||
const nextLevel = nextLevelResult.rows[0]
|
const nextLevel = nextLevelResult.rows[0]
|
||||||
const nextLevelId = Number(nextLevel?.id || 0)
|
const nextLevelId = Number(nextLevel?.id || 0)
|
||||||
@@ -1176,7 +1182,13 @@ export async function maybeUpgradeWorkerLevelWithClient(
|
|||||||
worker_id, from_level_id, to_level_id, reason, payload_json, created_at
|
worker_id, from_level_id, to_level_id, reason, payload_json, created_at
|
||||||
) VALUES ($1, $2, $3, 'auto', $4::jsonb, $5)
|
) VALUES ($1, $2, $3, 'auto', $4::jsonb, $5)
|
||||||
`,
|
`,
|
||||||
[workerId, currentRow.level_id || null, nextLevelId, JSON.stringify({ acceptedCount }), now],
|
[
|
||||||
|
workerId,
|
||||||
|
currentRow.level_id || null,
|
||||||
|
nextLevelId,
|
||||||
|
JSON.stringify({ acceptedCount, effectiveAcceptedCount }),
|
||||||
|
now,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
return getWorkerUserByIdWithClient(client, workerId)
|
return getWorkerUserByIdWithClient(client, workerId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,14 @@ import {
|
|||||||
listAdminWorkerLevels,
|
listAdminWorkerLevels,
|
||||||
listAdminWorkerUsers,
|
listAdminWorkerUsers,
|
||||||
listAdminWorkOrders,
|
listAdminWorkOrders,
|
||||||
|
listAdminWorkerCancelRequests,
|
||||||
markAdminWorkOrderProblem,
|
markAdminWorkOrderProblem,
|
||||||
pinAdminWorkOrder,
|
pinAdminWorkOrder,
|
||||||
publishAdminWorkOrder,
|
publishAdminWorkOrder,
|
||||||
reopenAdminCancelledWorkOrder,
|
reopenAdminCancelledWorkOrder,
|
||||||
reprocessAdminKuaishouSendCodeWorkOrders,
|
reprocessAdminKuaishouSendCodeWorkOrders,
|
||||||
reviewAdminWorkerFinanceRequest,
|
reviewAdminWorkerFinanceRequest,
|
||||||
|
reviewAdminWorkerCancelRequest,
|
||||||
resolveAdminProblemWorkOrder,
|
resolveAdminProblemWorkOrder,
|
||||||
reviewAdminWorkerUser,
|
reviewAdminWorkerUser,
|
||||||
resetAdminWorkerPassword,
|
resetAdminWorkerPassword,
|
||||||
@@ -483,6 +485,40 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/worker-platform/cancel-requests',
|
||||||
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
|
createJsonHandler((req) => listAdminWorkerCancelRequests(req.query), {
|
||||||
|
successMessage: 'ok',
|
||||||
|
errorMessage: '读取撤单申请失败',
|
||||||
|
scope: '[admin/worker-platform/cancel-requests]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/worker-platform/cancel-requests/:requestId/review',
|
||||||
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
|
createJsonHandler(
|
||||||
|
(req) =>
|
||||||
|
reviewAdminWorkerCancelRequest(
|
||||||
|
String(req.params.requestId || ''),
|
||||||
|
req.body || {},
|
||||||
|
req.adminSession?.username || '',
|
||||||
|
),
|
||||||
|
{
|
||||||
|
successMessage: '撤单申请已审核',
|
||||||
|
errorMessage: '审核撤单申请失败',
|
||||||
|
scope: '[admin/worker-platform/cancel-requests/:requestId/review]',
|
||||||
|
audit: (req, data) => ({
|
||||||
|
action: 'worker_cancel_request_reviewed',
|
||||||
|
targetType: 'worker_cancel_request',
|
||||||
|
targetId: String(req.params.requestId || ''),
|
||||||
|
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/mock',
|
'/worker-platform/orders/mock',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import {
|
|||||||
remindWorkerOrderAcceptance,
|
remindWorkerOrderAcceptance,
|
||||||
supplementWorkerAcceptedOrderEvidence,
|
supplementWorkerAcceptedOrderEvidence,
|
||||||
submitWorkerOrderAcceptance,
|
submitWorkerOrderAcceptance,
|
||||||
|
getWorkerCancelRequestSummary,
|
||||||
|
submitWorkerCancelRequest,
|
||||||
} from '../services/worker-platform/index.js'
|
} from '../services/worker-platform/index.js'
|
||||||
import { buildNotFoundPayload, createRouteHandler } from '../utils/http.js'
|
import { buildNotFoundPayload, createRouteHandler } from '../utils/http.js'
|
||||||
import {
|
import {
|
||||||
@@ -440,6 +442,34 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/orders/cancel-requests',
|
||||||
|
requireActiveWorker,
|
||||||
|
createRouteHandler((req) => getWorkerCancelRequestSummary(getRequiredWorkerSession(req)), {
|
||||||
|
successMessage: 'ok',
|
||||||
|
errorMessage: '读取取消申请失败',
|
||||||
|
scope: '[worker/orders/cancel-requests]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/orders/:workOrderId/cancel-requests',
|
||||||
|
requireActiveWorker,
|
||||||
|
createRouteHandler(
|
||||||
|
(req) =>
|
||||||
|
submitWorkerCancelRequest(
|
||||||
|
String(req.params.workOrderId || ''),
|
||||||
|
req.body || {},
|
||||||
|
getRequiredWorkerSession(req),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
successMessage: '取消申请已提交,等待客服审核',
|
||||||
|
errorMessage: '提交取消申请失败',
|
||||||
|
scope: '[worker/orders/:workOrderId/cancel-requests]',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/orders/:workOrderId/events',
|
'/orders/:workOrderId/events',
|
||||||
requireActiveWorker,
|
requireActiveWorker,
|
||||||
|
|||||||
@@ -96,6 +96,32 @@ export async function createWorkerAcceptanceAdminNotification(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createWorkerCancelRequestAdminNotification(input: {
|
||||||
|
requestId: number
|
||||||
|
workOrderId: number
|
||||||
|
workOrderNo: string
|
||||||
|
productName: string
|
||||||
|
workerName: string
|
||||||
|
reason: string
|
||||||
|
}) {
|
||||||
|
return createConfiguredWorkerPlatformNotification('cancel_requested', {
|
||||||
|
notificationType: 'worker_cancel_requested',
|
||||||
|
priority: 'high',
|
||||||
|
entityType: 'worker_cancel_request',
|
||||||
|
entityId: input.requestId,
|
||||||
|
dedupeKey: `worker_cancel_request:${input.requestId}`,
|
||||||
|
title: '有打手撤单申请待审核',
|
||||||
|
body: [
|
||||||
|
`订单:${input.workOrderNo || input.workOrderId}`,
|
||||||
|
`商品:${input.productName || '-'}`,
|
||||||
|
`打手:${input.workerName || '-'}`,
|
||||||
|
`原因:${input.reason || '-'}`,
|
||||||
|
].join(';'),
|
||||||
|
actionPath: `${ADMIN_WORKER_PLATFORM_PATH}?tab=cancel-requests&cancelRequestId=${input.requestId}`,
|
||||||
|
now: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function remindWorkerAcceptanceAdminNotification(workOrderId: number) {
|
export async function remindWorkerAcceptanceAdminNotification(workOrderId: number) {
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const eventConfig = getWorkerPlatformNotificationConfig().events.acceptance_reminded
|
const eventConfig = getWorkerPlatformNotificationConfig().events.acceptance_reminded
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ import {
|
|||||||
resolveProblemWorkOrder,
|
resolveProblemWorkOrder,
|
||||||
reviewWorkerFinanceRequest,
|
reviewWorkerFinanceRequest,
|
||||||
reopenCancelledWorkOrder,
|
reopenCancelledWorkOrder,
|
||||||
|
listAdminWorkerCancelRequests as listWorkerCancelRequestsForAdmin,
|
||||||
|
reviewWorkerCancelRequest,
|
||||||
updateWorkOrder,
|
updateWorkOrder,
|
||||||
updateWorkOrderBasic,
|
updateWorkOrderBasic,
|
||||||
updateWorkerUser,
|
updateWorkerUser,
|
||||||
@@ -108,6 +110,7 @@ import {
|
|||||||
mapWorkProductRule,
|
mapWorkProductRule,
|
||||||
mapWorkerLevel,
|
mapWorkerLevel,
|
||||||
mapWorkerUser,
|
mapWorkerUser,
|
||||||
|
mapWorkerCancelRequest,
|
||||||
normalizeAdminFinanceReviewStatus,
|
normalizeAdminFinanceReviewStatus,
|
||||||
normalizeAmountFen,
|
normalizeAmountFen,
|
||||||
normalizeBoolean,
|
normalizeBoolean,
|
||||||
@@ -1355,6 +1358,74 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listAdminWorkerCancelRequests(query: JsonObject = {}) {
|
||||||
|
const status = String(query.status || 'pending').trim()
|
||||||
|
const normalizedStatus = ['pending', 'approved', 'rejected'].includes(status) ? status : 'pending'
|
||||||
|
return {
|
||||||
|
items: (await listWorkerCancelRequestsForAdmin(normalizedStatus)).map(mapWorkerCancelRequest),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reviewAdminWorkerCancelRequest(
|
||||||
|
requestId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
actorName = '',
|
||||||
|
) {
|
||||||
|
const approved = payload.approved === true || String(payload.action || '').trim() === 'approve'
|
||||||
|
const reviewNote = String(payload.reviewNote || payload.note || '').trim()
|
||||||
|
if (!reviewNote) {
|
||||||
|
throw createHttpError('请填写审核备注', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_cancel_request_review_note_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const result = await reviewWorkerCancelRequest({
|
||||||
|
requestId: Number(requestId),
|
||||||
|
approved,
|
||||||
|
reviewNote,
|
||||||
|
actorName,
|
||||||
|
now: nowIso(),
|
||||||
|
})
|
||||||
|
if (result.failureReason === 'request_not_found') {
|
||||||
|
throw createHttpError('撤单申请不存在', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'worker_cancel_request_not_found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (result.failureReason === 'request_not_pending') {
|
||||||
|
throw createHttpError('该撤单申请已审核,请刷新后重试', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_cancel_request_not_pending',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!result.request || !result.order) {
|
||||||
|
throw createHttpError('订单状态已变化,无法审核该撤单申请', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_cancel_request_target_changed',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await resolveAdminNotificationEntity(
|
||||||
|
'worker_cancel_request',
|
||||||
|
Number(result.request.id),
|
||||||
|
approved ? 'approved' : 'rejected',
|
||||||
|
)
|
||||||
|
const workerId = Number(result.request.worker_id)
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(result.order.id),
|
||||||
|
workerIds: workerId > 0 ? [workerId] : [],
|
||||||
|
hallChanged: approved,
|
||||||
|
})
|
||||||
|
if (approved && workerId > 0) publishWorkerWalletRealtimeChange(workerId)
|
||||||
|
return {
|
||||||
|
request: mapWorkerCancelRequest(result.request),
|
||||||
|
order: mapWorkOrderAdmin(result.order),
|
||||||
|
approved,
|
||||||
|
releasedDepositAmount: result.releasedDepositAmount,
|
||||||
|
levelChanged: result.levelChanged,
|
||||||
|
accountDisabled: result.accountDisabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function deductAdminWorkOrderPendingDeposit(
|
export async function deductAdminWorkOrderPendingDeposit(
|
||||||
workOrderId: number | string,
|
workOrderId: number | string,
|
||||||
payload: JsonObject = {},
|
payload: JsonObject = {},
|
||||||
|
|||||||
@@ -257,6 +257,41 @@ export function mapWorkerUser(worker: WorkerUserRow) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapWorkerCancelRequest(request: {
|
||||||
|
id: number
|
||||||
|
work_order_id: number
|
||||||
|
work_order_share_id: number | null
|
||||||
|
worker_id: number
|
||||||
|
reason: string
|
||||||
|
status: string
|
||||||
|
review_note: string
|
||||||
|
reviewed_by: string
|
||||||
|
created_at: string
|
||||||
|
reviewed_at: string | null
|
||||||
|
work_order_no?: string
|
||||||
|
product_name?: string
|
||||||
|
worker_username?: string
|
||||||
|
worker_display_name?: string
|
||||||
|
share_quantity?: number | null
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
requestId: Number(request.id),
|
||||||
|
workOrderId: Number(request.work_order_id),
|
||||||
|
shareId: request.work_order_share_id ? Number(request.work_order_share_id) : null,
|
||||||
|
workerId: Number(request.worker_id),
|
||||||
|
workOrderNo: String(request.work_order_no || ''),
|
||||||
|
productName: String(request.product_name || ''),
|
||||||
|
workerName: String(request.worker_display_name || request.worker_username || ''),
|
||||||
|
shareQuantity: request.share_quantity ? Number(request.share_quantity) : null,
|
||||||
|
reason: String(request.reason || ''),
|
||||||
|
status: String(request.status || ''),
|
||||||
|
reviewNote: String(request.review_note || ''),
|
||||||
|
reviewedBy: String(request.reviewed_by || ''),
|
||||||
|
createdAt: request.created_at,
|
||||||
|
reviewedAt: request.reviewed_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function mapWalletLedger(ledger: WorkerWalletLedgerRow | null | undefined) {
|
export function mapWalletLedger(ledger: WorkerWalletLedgerRow | null | undefined) {
|
||||||
if (!ledger) return null
|
if (!ledger) return null
|
||||||
return {
|
return {
|
||||||
|
|||||||
+2
@@ -8,6 +8,7 @@ export const WORKER_PLATFORM_NOTIFICATION_EVENT_KEYS = [
|
|||||||
'acceptance_submitted',
|
'acceptance_submitted',
|
||||||
'acceptance_reminded',
|
'acceptance_reminded',
|
||||||
'material_required',
|
'material_required',
|
||||||
|
'cancel_requested',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type WorkerPlatformNotificationEventKey =
|
export type WorkerPlatformNotificationEventKey =
|
||||||
@@ -51,6 +52,7 @@ export function createDefaultWorkerPlatformNotificationConfig(): WorkerPlatformN
|
|||||||
soundEnabled: false,
|
soundEnabled: false,
|
||||||
externalPushEnabled: false,
|
externalPushEnabled: false,
|
||||||
},
|
},
|
||||||
|
cancel_requested: createEnabledEventConfig(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
acceptWorkOrderAndSettle,
|
acceptWorkOrderAndSettle,
|
||||||
acceptWorkOrderShareAndSettle,
|
acceptWorkOrderShareAndSettle,
|
||||||
countWorkerAcceptedOrders,
|
countWorkerAcceptedOrders,
|
||||||
|
createWorkerCancelRequest,
|
||||||
countWorkerActiveOrders,
|
countWorkerActiveOrders,
|
||||||
countWorkerTimeoutEvents,
|
countWorkerTimeoutEvents,
|
||||||
countWorkerWithdrawRequestsOnDay,
|
countWorkerWithdrawRequestsOnDay,
|
||||||
@@ -42,6 +43,7 @@ import {
|
|||||||
listPendingMaterialWorkOrdersByPlatformOrderId,
|
listPendingMaterialWorkOrdersByPlatformOrderId,
|
||||||
listWorkerFinanceRequests,
|
listWorkerFinanceRequests,
|
||||||
listWorkerCompletionLeaderboard,
|
listWorkerCompletionLeaderboard,
|
||||||
|
listWorkerCancelRequests,
|
||||||
listWorkerSharesByWorker,
|
listWorkerSharesByWorker,
|
||||||
listWorkerWalletLedgers,
|
listWorkerWalletLedgers,
|
||||||
listWorkerWorkOrderNotes,
|
listWorkerWorkOrderNotes,
|
||||||
@@ -53,6 +55,7 @@ import {
|
|||||||
listWorkOrderShares,
|
listWorkOrderShares,
|
||||||
listWorkOrderSharesByOrderIds,
|
listWorkOrderSharesByOrderIds,
|
||||||
listWorkerLevels,
|
listWorkerLevels,
|
||||||
|
getWorkerCancelRequestRisk,
|
||||||
listDueDepositUnfreezes,
|
listDueDepositUnfreezes,
|
||||||
listOverdueWorkOrders,
|
listOverdueWorkOrders,
|
||||||
releaseDepositUnfreeze,
|
releaseDepositUnfreeze,
|
||||||
@@ -82,6 +85,7 @@ import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
|||||||
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
||||||
import {
|
import {
|
||||||
createWorkerAcceptanceAdminNotification,
|
createWorkerAcceptanceAdminNotification,
|
||||||
|
createWorkerCancelRequestAdminNotification,
|
||||||
createWorkerRechargeAdminNotification,
|
createWorkerRechargeAdminNotification,
|
||||||
createWorkerWithdrawAdminNotification,
|
createWorkerWithdrawAdminNotification,
|
||||||
getWorkerAcceptanceReminder,
|
getWorkerAcceptanceReminder,
|
||||||
@@ -111,6 +115,7 @@ import {
|
|||||||
mapWorkOrderPublic,
|
mapWorkOrderPublic,
|
||||||
mapWorkOrderEvents,
|
mapWorkOrderEvents,
|
||||||
mapWorkOrderShare,
|
mapWorkOrderShare,
|
||||||
|
mapWorkerCancelRequest,
|
||||||
mapWorkerUser,
|
mapWorkerUser,
|
||||||
normalizeAmountFen,
|
normalizeAmountFen,
|
||||||
assertWithdrawChannelAllowedForAmount,
|
assertWithdrawChannelAllowedForAmount,
|
||||||
@@ -1738,6 +1743,82 @@ export async function getWorkerMyOrderEvents(workOrderId: number | string, sessi
|
|||||||
return { items: await mapWorkOrderEvents(events) }
|
return { items: await mapWorkOrderEvents(events) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getWorkerCancelRequestSummary(session: WorkerSession) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const [risk, requests] = await Promise.all([
|
||||||
|
getWorkerCancelRequestRisk(session.workerId, nowIso()),
|
||||||
|
listWorkerCancelRequests(session.workerId),
|
||||||
|
])
|
||||||
|
return { risk, items: requests.map(mapWorkerCancelRequest) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitWorkerCancelRequest(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
session: WorkerSession,
|
||||||
|
) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const reason = String(payload.reason || '').trim()
|
||||||
|
if (!reason) {
|
||||||
|
throw createHttpError('请填写申请取消原因', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_cancel_request_reason_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (reason.length > 300) {
|
||||||
|
throw createHttpError('申请取消原因不能超过 300 个字符', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_cancel_request_reason_too_long',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const now = nowIso()
|
||||||
|
const result = await createWorkerCancelRequest({
|
||||||
|
workOrderId: Number(workOrderId),
|
||||||
|
workerId: Number(session.workerId),
|
||||||
|
reason,
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
if (result.failureReason === 'daily_limit_reached') {
|
||||||
|
throw createHttpError('今日最多申请取消 5 次,请明日再试', {
|
||||||
|
statusCode: 429,
|
||||||
|
errorCode: 'worker_cancel_request_daily_limit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (result.failureReason === 'request_pending') {
|
||||||
|
throw createHttpError('该订单已有待审核的取消申请', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_cancel_request_pending',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (result.failureReason === 'share_not_cancellable') {
|
||||||
|
throw createHttpError('当前拼单份额不能申请取消', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_cancel_request_share_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!result.request) {
|
||||||
|
throw createHttpError('当前订单不能申请取消', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_cancel_request_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
|
await createWorkerCancelRequestAdminNotification({
|
||||||
|
requestId: Number(result.request.id),
|
||||||
|
workOrderId: Number(result.request.work_order_id),
|
||||||
|
workOrderNo: result.request.work_order_no || '',
|
||||||
|
productName: result.request.product_name || '',
|
||||||
|
workerName: worker.display_name || worker.username,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
const risk = await getWorkerCancelRequestRisk(session.workerId, now)
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(result.request.work_order_id),
|
||||||
|
workerIds: [Number(session.workerId)],
|
||||||
|
})
|
||||||
|
return { request: mapWorkerCancelRequest(result.request), risk }
|
||||||
|
}
|
||||||
|
|
||||||
export async function saveWorkerOrderNote(
|
export async function saveWorkerOrderNote(
|
||||||
workOrderId: number | string,
|
workOrderId: number | string,
|
||||||
payload: JsonObject = {},
|
payload: JsonObject = {},
|
||||||
|
|||||||
@@ -95,6 +95,21 @@ export function formatWorkOrderEvent(event: WorkOrderEvent): EventNodeStyle {
|
|||||||
}
|
}
|
||||||
case 'cancelled_by_worker':
|
case 'cancelled_by_worker':
|
||||||
return { title: '打手取消接单', color: 'red' }
|
return { title: '打手取消接单', color: 'red' }
|
||||||
|
case 'cancel_request_submitted':
|
||||||
|
return {
|
||||||
|
title: note ? `打手提交撤单申请:${note}` : '打手提交撤单申请,等待客服审核',
|
||||||
|
color: 'orange',
|
||||||
|
}
|
||||||
|
case 'cancel_request_rejected':
|
||||||
|
return {
|
||||||
|
title: note ? `客服驳回撤单申请:${note}` : '客服驳回撤单申请',
|
||||||
|
color: 'green',
|
||||||
|
}
|
||||||
|
case 'cancel_request_approved':
|
||||||
|
return {
|
||||||
|
title: note ? `客服同意撤单申请:${note}` : '客服同意撤单申请,订单已回到大厅',
|
||||||
|
color: 'red',
|
||||||
|
}
|
||||||
case 'timeout_reopen':
|
case 'timeout_reopen':
|
||||||
return { title: '订单超时未完成,已退回抢单大厅', color: 'orange' }
|
return { title: '订单超时未完成,已退回抢单大厅', color: 'orange' }
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import PageHeader from '@/components/admin/PageHeader'
|
|||||||
import { fetchAdminWorkerPlatformSummary } from '@/services/admin'
|
import { fetchAdminWorkerPlatformSummary } from '@/services/admin'
|
||||||
import { getAdminRole } from '@/utils/admin-auth'
|
import { getAdminRole } from '@/utils/admin-auth'
|
||||||
import CategoriesPanel from './panels/CategoriesPanel'
|
import CategoriesPanel from './panels/CategoriesPanel'
|
||||||
|
import CancelRequestsPanel from './panels/CancelRequestsPanel'
|
||||||
import FinancePanel from './panels/FinancePanel'
|
import FinancePanel from './panels/FinancePanel'
|
||||||
import LevelsPanel from './panels/LevelsPanel'
|
import LevelsPanel from './panels/LevelsPanel'
|
||||||
import NotificationsPanel from './panels/NotificationsPanel'
|
import NotificationsPanel from './panels/NotificationsPanel'
|
||||||
@@ -21,8 +22,7 @@ export default function AdminWorkerPlatformPage() {
|
|||||||
const role = getAdminRole()
|
const role = getAdminRole()
|
||||||
const isAdmin = role === 'admin'
|
const isAdmin = role === 'admin'
|
||||||
const isSupport = role === 'support'
|
const isSupport = role === 'support'
|
||||||
const visibleActiveTab =
|
const visibleActiveTab = !isAdmin && activeTab === 'notifications' ? 'orders' : activeTab
|
||||||
isSupport || (!isAdmin && activeTab === 'notifications') ? 'orders' : activeTab
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tab = searchParams.get('tab')
|
const tab = searchParams.get('tab')
|
||||||
@@ -46,9 +46,13 @@ export default function AdminWorkerPlatformPage() {
|
|||||||
onChange={changeTab}
|
onChange={changeTab}
|
||||||
items={
|
items={
|
||||||
isSupport
|
isSupport
|
||||||
? [{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> }]
|
? [
|
||||||
|
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
||||||
|
{ key: 'cancel-requests', label: '撤单审核', children: <CancelRequestsPanel /> },
|
||||||
|
]
|
||||||
: [
|
: [
|
||||||
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
||||||
|
{ key: 'cancel-requests', label: '撤单审核', children: <CancelRequestsPanel /> },
|
||||||
{ key: 'rules', label: '接单模板', children: <ProductRulesPanel /> },
|
{ key: 'rules', label: '接单模板', children: <ProductRulesPanel /> },
|
||||||
{ key: 'product-match', label: '匹配诊断', children: <ProductMatchPanel /> },
|
{ key: 'product-match', label: '匹配诊断', children: <ProductMatchPanel /> },
|
||||||
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
|
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
|
||||||
@@ -70,6 +74,7 @@ const ACTIVE_TAB_STORAGE_KEY = 'admin-worker-platform-tab'
|
|||||||
function loadActiveTabPreference(): string {
|
function loadActiveTabPreference(): string {
|
||||||
const saved = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
|
const saved = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
|
||||||
return saved === 'orders' ||
|
return saved === 'orders' ||
|
||||||
|
saved === 'cancel-requests' ||
|
||||||
saved === 'rules' ||
|
saved === 'rules' ||
|
||||||
saved === 'product-match' ||
|
saved === 'product-match' ||
|
||||||
saved === 'categories' ||
|
saved === 'categories' ||
|
||||||
@@ -88,6 +93,7 @@ function loadActiveTab(tab: string | null): string {
|
|||||||
function isWorkerPlatformTab(tab: string | null): tab is string {
|
function isWorkerPlatformTab(tab: string | null): tab is string {
|
||||||
return [
|
return [
|
||||||
'orders',
|
'orders',
|
||||||
|
'cancel-requests',
|
||||||
'rules',
|
'rules',
|
||||||
'product-match',
|
'product-match',
|
||||||
'categories',
|
'categories',
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { ReloadOutlined } from '@ant-design/icons'
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { App, Button, Card, Form, Input, Modal, Space, Table, Tag, Typography } from 'antd'
|
||||||
|
import type { TableColumnsType } from 'antd'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useSearchParams } from 'react-router'
|
||||||
|
|
||||||
|
import { fetchAdminWorkerCancelRequests, reviewAdminWorkerCancelRequest } from '@/services/admin'
|
||||||
|
import type { WorkerCancelRequest } from '@/types/worker-platform'
|
||||||
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
|
|
||||||
|
type ReviewFormValues = {
|
||||||
|
reviewNote?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CancelRequestsPanel() {
|
||||||
|
const { message } = App.useApp()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const [reviewTarget, setReviewTarget] = useState<{
|
||||||
|
request: WorkerCancelRequest
|
||||||
|
approved: boolean
|
||||||
|
} | null>(null)
|
||||||
|
const [form] = Form.useForm<ReviewFormValues>()
|
||||||
|
const requestsQuery = useQuery({
|
||||||
|
queryKey: ['admin-worker-cancel-requests', 'pending'],
|
||||||
|
queryFn: () => fetchAdminWorkerCancelRequests('pending'),
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const requestId = Number(searchParams.get('cancelRequestId') || 0)
|
||||||
|
if (!Number.isInteger(requestId) || requestId <= 0) return
|
||||||
|
const request = (requestsQuery.data?.data.items || []).find(
|
||||||
|
(item) => item.requestId === requestId,
|
||||||
|
)
|
||||||
|
if (!request) return
|
||||||
|
setReviewTarget({ request, approved: true })
|
||||||
|
form.resetFields()
|
||||||
|
setSearchParams(
|
||||||
|
(current) => {
|
||||||
|
current.delete('cancelRequestId')
|
||||||
|
return current
|
||||||
|
},
|
||||||
|
{ replace: true },
|
||||||
|
)
|
||||||
|
}, [form, requestsQuery.data, searchParams, setSearchParams])
|
||||||
|
|
||||||
|
async function refreshAll() {
|
||||||
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['admin-worker-cancel-requests'] }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-orders'] }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-summary'] }),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function openReview(request: WorkerCancelRequest, approved: boolean) {
|
||||||
|
setReviewTarget({ request, approved })
|
||||||
|
form.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitReview(values: ReviewFormValues) {
|
||||||
|
if (!reviewTarget) return
|
||||||
|
try {
|
||||||
|
const response = await reviewAdminWorkerCancelRequest(reviewTarget.request.requestId, {
|
||||||
|
approved: reviewTarget.approved,
|
||||||
|
reviewNote: String(values.reviewNote || '').trim(),
|
||||||
|
})
|
||||||
|
const result = response.data
|
||||||
|
const resultText = result.accountDisabled
|
||||||
|
? ',该打手已封禁'
|
||||||
|
: result.levelChanged
|
||||||
|
? ',该打手已降一级 VIP'
|
||||||
|
: ''
|
||||||
|
message.success(`${result.approved ? '已同意' : '已驳回'}撤单申请${resultText}`)
|
||||||
|
setReviewTarget(null)
|
||||||
|
form.resetFields()
|
||||||
|
await refreshAll()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '审核撤单申请失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: TableColumnsType<WorkerCancelRequest> = [
|
||||||
|
{
|
||||||
|
title: '订单',
|
||||||
|
width: 260,
|
||||||
|
render: (_, request) => (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text strong>{request.productName || '-'}</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">{request.workOrderNo || '-'}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '打手 / 份数',
|
||||||
|
width: 180,
|
||||||
|
render: (_, request) => (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text>{request.workerName || `打手 #${request.workerId}`}</Typography.Text>
|
||||||
|
{request.shareQuantity ? (
|
||||||
|
<Tag color="purple">拼单 {request.shareQuantity} 份</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag>整单</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '申请原因',
|
||||||
|
render: (_, request) => (
|
||||||
|
<Typography.Text ellipsis={{ tooltip: request.reason }}>{request.reason}</Typography.Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '申请时间',
|
||||||
|
width: 180,
|
||||||
|
render: (_, request) => formatAdminDateTime(request.createdAt),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 180,
|
||||||
|
render: (_, request) => (
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" size="small" onClick={() => openReview(request, true)}>
|
||||||
|
同意
|
||||||
|
</Button>
|
||||||
|
<Button danger size="small" onClick={() => openReview(request, false)}>
|
||||||
|
驳回
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card
|
||||||
|
title={`待审核撤单申请(${requestsQuery.data?.data.items.length || 0})`}
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
loading={requestsQuery.isFetching}
|
||||||
|
onClick={() => requestsQuery.refetch()}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
bordered={false}
|
||||||
|
>
|
||||||
|
<Table<WorkerCancelRequest>
|
||||||
|
rowKey="requestId"
|
||||||
|
size="small"
|
||||||
|
loading={requestsQuery.isLoading}
|
||||||
|
dataSource={requestsQuery.data?.data.items || []}
|
||||||
|
columns={columns}
|
||||||
|
pagination={false}
|
||||||
|
locale={{ emptyText: '暂无待审核撤单申请' }}
|
||||||
|
scroll={{ x: 900 }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Modal
|
||||||
|
title={reviewTarget?.approved ? '同意撤单申请' : '驳回撤单申请'}
|
||||||
|
open={Boolean(reviewTarget)}
|
||||||
|
onCancel={() => {
|
||||||
|
setReviewTarget(null)
|
||||||
|
form.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => form.submit()}
|
||||||
|
okText={reviewTarget?.approved ? '确认同意' : '确认驳回'}
|
||||||
|
okButtonProps={reviewTarget?.approved ? undefined : { danger: true }}
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
{reviewTarget ? (
|
||||||
|
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||||
|
<Typography.Text>
|
||||||
|
{reviewTarget.request.workerName || `打手 #${reviewTarget.request.workerId}`}:
|
||||||
|
{reviewTarget.request.reason}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type={reviewTarget.approved ? 'warning' : 'secondary'}>
|
||||||
|
{reviewTarget.approved
|
||||||
|
? '同意后将退还冻结押金,并将整单或对应拼单份额回到大厅;该申请会计入撤单处罚次数。'
|
||||||
|
: '驳回后不会计入累计次数,且会释放本次申请的当天额度。'}
|
||||||
|
</Typography.Text>
|
||||||
|
<Form form={form} layout="vertical" onFinish={submitReview}>
|
||||||
|
<Form.Item
|
||||||
|
label="审核备注"
|
||||||
|
name="reviewNote"
|
||||||
|
rules={[{ required: true, whitespace: true, message: '请填写审核备注' }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={3}
|
||||||
|
maxLength={300}
|
||||||
|
showCount
|
||||||
|
placeholder="请填写审核结论或说明"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Space>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ const notificationEventRows: NotificationEventRow[] = [
|
|||||||
{ key: 'acceptance_submitted', name: '提交验收', description: '打手提交订单验收资料' },
|
{ key: 'acceptance_submitted', name: '提交验收', description: '打手提交订单验收资料' },
|
||||||
{ key: 'acceptance_reminded', name: '催验收', description: '打手在验收后催促处理' },
|
{ key: 'acceptance_reminded', name: '催验收', description: '打手在验收后催促处理' },
|
||||||
{ key: 'material_required', name: '待补资料工单', description: '新工单等待后台补充资料' },
|
{ key: 'material_required', name: '待补资料工单', description: '新工单等待后台补充资料' },
|
||||||
|
{ key: 'cancel_requested', name: '撤单申请', description: '打手提交新的撤单申请,等待客服审核' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function NotificationsPanel() {
|
export default function NotificationsPanel() {
|
||||||
|
|||||||
@@ -267,7 +267,6 @@ export default function WorkOrdersPanel() {
|
|||||||
}),
|
}),
|
||||||
enabled: Boolean(assignOrder),
|
enabled: Boolean(assignOrder),
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedAcceptanceOrderIds([])
|
setSelectedAcceptanceOrderIds([])
|
||||||
}, [statuses, keyword, workerId, workerLevelIds, targetWorkOrderId, page, pageSize])
|
}, [statuses, keyword, workerId, workerLevelIds, targetWorkOrderId, page, pageSize])
|
||||||
@@ -1327,7 +1326,6 @@ export default function WorkOrdersPanel() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={`编辑工单 · ${getDisplayOrderNo(editOrder)}`}
|
title={`编辑工单 · ${getDisplayOrderNo(editOrder)}`}
|
||||||
open={Boolean(editOrder)}
|
open={Boolean(editOrder)}
|
||||||
@@ -2012,7 +2010,10 @@ function renderWorkOrderWorkers(order: WorkOrder) {
|
|||||||
const names = order.sharingWorkers
|
const names = order.sharingWorkers
|
||||||
.map((worker) => worker.displayName || worker.username || `打手#${worker.workerId}`)
|
.map((worker) => worker.displayName || worker.username || `打手#${worker.workerId}`)
|
||||||
.join('、')
|
.join('、')
|
||||||
const quantity = order.sharingWorkers.reduce((sum, worker) => sum + Number(worker.quantity || 0), 0)
|
const quantity = order.sharingWorkers.reduce(
|
||||||
|
(sum, worker) => sum + Number(worker.quantity || 0),
|
||||||
|
0,
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<Typography.Text ellipsis={{ tooltip: names }}>{names}</Typography.Text>
|
<Typography.Text ellipsis={{ tooltip: names }}>{names}</Typography.Text>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline'
|
|||||||
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||||
import ImageUpload from '@/components/files/ImageUpload'
|
import ImageUpload from '@/components/files/ImageUpload'
|
||||||
import {
|
import {
|
||||||
|
fetchWorkerCancelRequests,
|
||||||
fetchWorkerMyOrderOverview,
|
fetchWorkerMyOrderOverview,
|
||||||
fetchWorkerMyOrders,
|
fetchWorkerMyOrders,
|
||||||
fetchWorkerProfile,
|
fetchWorkerProfile,
|
||||||
@@ -43,6 +44,7 @@ import {
|
|||||||
saveWorkerOrderNote,
|
saveWorkerOrderNote,
|
||||||
supplementWorkerAcceptedOrderEvidence,
|
supplementWorkerAcceptedOrderEvidence,
|
||||||
submitWorkerAcceptance,
|
submitWorkerAcceptance,
|
||||||
|
submitWorkerCancelRequest,
|
||||||
} from '@/services/worker'
|
} from '@/services/worker'
|
||||||
import type {
|
import type {
|
||||||
CollectField,
|
CollectField,
|
||||||
@@ -73,6 +75,10 @@ type WorkerNoteFormValues = {
|
|||||||
note?: string
|
note?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CancelRequestFormValues = {
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
function buildWorkerOrderQuery(status: string, keyword: string, page: number, pageSize: number) {
|
function buildWorkerOrderQuery(status: string, keyword: string, page: number, pageSize: number) {
|
||||||
return {
|
return {
|
||||||
keyword,
|
keyword,
|
||||||
@@ -194,9 +200,11 @@ export default function WorkerOrdersPage() {
|
|||||||
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
|
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
|
||||||
const [editingOrder, setEditingOrder] = useState<WorkOrder | null>(null)
|
const [editingOrder, setEditingOrder] = useState<WorkOrder | null>(null)
|
||||||
const [noteOrder, setNoteOrder] = useState<WorkOrder | null>(null)
|
const [noteOrder, setNoteOrder] = useState<WorkOrder | null>(null)
|
||||||
|
const [cancelRequestOrder, setCancelRequestOrder] = useState<WorkOrder | null>(null)
|
||||||
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
|
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
|
||||||
const [form] = Form.useForm<AcceptanceFormValues>()
|
const [form] = Form.useForm<AcceptanceFormValues>()
|
||||||
const [noteForm] = Form.useForm<WorkerNoteFormValues>()
|
const [noteForm] = Form.useForm<WorkerNoteFormValues>()
|
||||||
|
const [cancelRequestForm] = Form.useForm<CancelRequestFormValues>()
|
||||||
|
|
||||||
const ordersQuery = useQuery({
|
const ordersQuery = useQuery({
|
||||||
queryKey: ['worker-my-orders', status, keyword, page, pageSize],
|
queryKey: ['worker-my-orders', status, keyword, page, pageSize],
|
||||||
@@ -221,6 +229,11 @@ export default function WorkerOrdersPage() {
|
|||||||
queryFn: fetchWorkerMyOrderOverview,
|
queryFn: fetchWorkerMyOrderOverview,
|
||||||
retry: false,
|
retry: false,
|
||||||
})
|
})
|
||||||
|
const cancelRequestsQuery = useQuery({
|
||||||
|
queryKey: ['worker-cancel-requests'],
|
||||||
|
queryFn: fetchWorkerCancelRequests,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
const overviewByStatus = new Map(
|
const overviewByStatus = new Map(
|
||||||
(overviewQuery.data?.data.items || []).map((item) => [item.status, item]),
|
(overviewQuery.data?.data.items || []).map((item) => [item.status, item]),
|
||||||
)
|
)
|
||||||
@@ -229,6 +242,7 @@ export default function WorkerOrdersPage() {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] }),
|
queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] }),
|
||||||
queryClient.invalidateQueries({ queryKey: ['worker-my-orders-overview'] }),
|
queryClient.invalidateQueries({ queryKey: ['worker-my-orders-overview'] }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['worker-cancel-requests'] }),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +328,27 @@ export default function WorkerOrdersPage() {
|
|||||||
noteForm.setFieldsValue({ note: String(order.workerNote || '') })
|
noteForm.setFieldsValue({ note: String(order.workerNote || '') })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCancelRequestModal(order: WorkOrder) {
|
||||||
|
markOrderAsRead(order)
|
||||||
|
setCancelRequestOrder(order)
|
||||||
|
cancelRequestForm.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitCancelRequest(values: CancelRequestFormValues) {
|
||||||
|
if (!cancelRequestOrder) return
|
||||||
|
try {
|
||||||
|
await submitWorkerCancelRequest(cancelRequestOrder.workOrderId, {
|
||||||
|
reason: String(values.reason || '').trim(),
|
||||||
|
})
|
||||||
|
message.success('取消申请已提交,等待客服审核')
|
||||||
|
setCancelRequestOrder(null)
|
||||||
|
cancelRequestForm.resetFields()
|
||||||
|
await refreshAll()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '提交取消申请失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveOrderNote(values: WorkerNoteFormValues) {
|
async function saveOrderNote(values: WorkerNoteFormValues) {
|
||||||
if (!noteOrder) return
|
if (!noteOrder) return
|
||||||
try {
|
try {
|
||||||
@@ -644,6 +679,11 @@ export default function WorkerOrdersPage() {
|
|||||||
{canRemindAcceptance(row) ? (
|
{canRemindAcceptance(row) ? (
|
||||||
<Button onClick={() => submitAcceptanceReminder(row)}>催验收</Button>
|
<Button onClick={() => submitAcceptanceReminder(row)}>催验收</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{canRequestCancel(row) ? (
|
||||||
|
<Button danger onClick={() => openCancelRequestModal(row)}>
|
||||||
|
申请取消
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -950,6 +990,11 @@ export default function WorkerOrdersPage() {
|
|||||||
催验收
|
催验收
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{canRequestCancel(order) ? (
|
||||||
|
<Button size="small" danger onClick={() => openCancelRequestModal(order)}>
|
||||||
|
申请取消
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -1113,6 +1158,11 @@ export default function WorkerOrdersPage() {
|
|||||||
{hasSubmittedAcceptance(detailOrder) ? '补充图片' : '上传图片 / 提交验收'}
|
{hasSubmittedAcceptance(detailOrder) ? '补充图片' : '上传图片 / 提交验收'}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{canRequestCancel(detailOrder) ? (
|
||||||
|
<Button danger onClick={() => openCancelRequestModal(detailOrder)}>
|
||||||
|
申请取消
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
@@ -1229,6 +1279,46 @@ export default function WorkerOrdersPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
<Modal
|
||||||
|
title={`申请取消 · ${getDisplayOrderNo(cancelRequestOrder)}`}
|
||||||
|
open={Boolean(cancelRequestOrder)}
|
||||||
|
width={isMobile ? '92%' : 520}
|
||||||
|
onCancel={() => {
|
||||||
|
setCancelRequestOrder(null)
|
||||||
|
cancelRequestForm.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => cancelRequestForm.submit()}
|
||||||
|
okText="提交申请"
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={8} style={{ width: '100%', marginBottom: 16 }}>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
提交后由客服审核;通过后退还本次冻结押金,订单或拼单份额将回到大厅。
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
今日申请:{cancelRequestsQuery.data?.data.risk.dailyUsed || 0}/
|
||||||
|
{cancelRequestsQuery.data?.data.risk.dailyLimit || 5};累计通过:
|
||||||
|
{cancelRequestsQuery.data?.data.risk.approvedTotal || 0} 次。
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="warning">
|
||||||
|
每累计 10 次通过申请最多降一级;VIP1 期间累计 5 次通过将封禁账号。
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
<Form form={cancelRequestForm} layout="vertical" onFinish={submitCancelRequest}>
|
||||||
|
<Form.Item
|
||||||
|
label="申请原因"
|
||||||
|
name="reason"
|
||||||
|
rules={[{ required: true, whitespace: true, message: '请填写申请取消原因' }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={4}
|
||||||
|
maxLength={300}
|
||||||
|
showCount
|
||||||
|
placeholder="请说明无法继续处理的原因"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
title={`订单备注 · ${getDisplayOrderNo(noteOrder)}`}
|
title={`订单备注 · ${getDisplayOrderNo(noteOrder)}`}
|
||||||
open={Boolean(noteOrder)}
|
open={Boolean(noteOrder)}
|
||||||
@@ -1382,6 +1472,16 @@ function canRemindAcceptance(order: WorkOrder) {
|
|||||||
return !order.myShare && order.status === 'pending_acceptance'
|
return !order.myShare && order.status === 'pending_acceptance'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canRequestCancel(order: WorkOrder) {
|
||||||
|
if (order.myShare) {
|
||||||
|
return (
|
||||||
|
['joined', 'submitted'].includes(order.myShare.status) &&
|
||||||
|
['open', 'pending_acceptance'].includes(order.status)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return order.status === 'in_progress'
|
||||||
|
}
|
||||||
|
|
||||||
function canAutoAcceptOrder(order: WorkOrder, workerCanAutoAcceptWithoutEvidence: boolean) {
|
function canAutoAcceptOrder(order: WorkOrder, workerCanAutoAcceptWithoutEvidence: boolean) {
|
||||||
if (!workerCanAutoAcceptWithoutEvidence) return false
|
if (!workerCanAutoAcceptWithoutEvidence) return false
|
||||||
if (order.myShare) return order.myShare.status === 'joined' && order.status !== 'accepted'
|
if (order.myShare) return order.myShare.status === 'joined' && order.status !== 'accepted'
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type {
|
|||||||
WorkProductMatchLog,
|
WorkProductMatchLog,
|
||||||
WorkerFinanceConfig,
|
WorkerFinanceConfig,
|
||||||
WorkerFinanceRequest,
|
WorkerFinanceRequest,
|
||||||
|
WorkerCancelRequest,
|
||||||
WorkerPlatformNotificationConfig,
|
WorkerPlatformNotificationConfig,
|
||||||
WorkerProductMatchConfig,
|
WorkerProductMatchConfig,
|
||||||
WorkerLevel,
|
WorkerLevel,
|
||||||
@@ -39,6 +40,28 @@ export function fetchAdminWorkerPlatformSummary() {
|
|||||||
}>('/api/v1/admin/worker-platform/summary')
|
}>('/api/v1/admin/worker-platform/summary')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchAdminWorkerCancelRequests(
|
||||||
|
status: 'pending' | 'approved' | 'rejected' = 'pending',
|
||||||
|
) {
|
||||||
|
return apiGet<{ items: WorkerCancelRequest[] }>('/api/v1/admin/worker-platform/cancel-requests', {
|
||||||
|
status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reviewAdminWorkerCancelRequest(
|
||||||
|
requestId: number,
|
||||||
|
payload: { approved: boolean; reviewNote: string },
|
||||||
|
) {
|
||||||
|
return apiPost<{
|
||||||
|
request: WorkerCancelRequest
|
||||||
|
order: WorkOrder
|
||||||
|
approved: boolean
|
||||||
|
releasedDepositAmount: number
|
||||||
|
levelChanged: boolean
|
||||||
|
accountDisabled: boolean
|
||||||
|
}>(`/api/v1/admin/worker-platform/cancel-requests/${requestId}/review`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchAdminWorkerLevels() {
|
export function fetchAdminWorkerLevels() {
|
||||||
return apiGet<{ items: WorkerLevel[] }>('/api/v1/admin/worker-platform/levels')
|
return apiGet<{ items: WorkerLevel[] }>('/api/v1/admin/worker-platform/levels')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import type {
|
|||||||
WorkOrderEvent,
|
WorkOrderEvent,
|
||||||
WorkOrderShare,
|
WorkOrderShare,
|
||||||
WorkerFinanceRequest,
|
WorkerFinanceRequest,
|
||||||
|
WorkerCancelRequest,
|
||||||
|
WorkerCancelRequestRisk,
|
||||||
WorkerHallOrdersResponse,
|
WorkerHallOrdersResponse,
|
||||||
WorkerLeaderboardResponse,
|
WorkerLeaderboardResponse,
|
||||||
WorkerLeaderboardPeriod,
|
WorkerLeaderboardPeriod,
|
||||||
@@ -211,6 +213,19 @@ export function fetchWorkerMyOrderEvents(workOrderId: number) {
|
|||||||
return apiGet<{ items: WorkOrderEvent[] }>(`/api/v1/worker/orders/${workOrderId}/events`)
|
return apiGet<{ items: WorkOrderEvent[] }>(`/api/v1/worker/orders/${workOrderId}/events`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchWorkerCancelRequests() {
|
||||||
|
return apiGet<{ risk: WorkerCancelRequestRisk; items: WorkerCancelRequest[] }>(
|
||||||
|
'/api/v1/worker/orders/cancel-requests',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function submitWorkerCancelRequest(workOrderId: number, payload: { reason: string }) {
|
||||||
|
return apiPost<{ request: WorkerCancelRequest; risk: WorkerCancelRequestRisk }>(
|
||||||
|
`/api/v1/worker/orders/${workOrderId}/cancel-requests`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function markWorkerOrderRead(workOrderId: number) {
|
export function markWorkerOrderRead(workOrderId: number) {
|
||||||
return apiPost<{ viewedAt: string; isUnread: false }>(`/api/v1/worker/orders/${workOrderId}/read`)
|
return apiPost<{ viewedAt: string; isUnread: false }>(`/api/v1/worker/orders/${workOrderId}/read`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,31 @@ export type WorkerFinanceRequest = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkerCancelRequest = {
|
||||||
|
requestId: number
|
||||||
|
workOrderId: number
|
||||||
|
shareId: number | null
|
||||||
|
workerId: number
|
||||||
|
workOrderNo: string
|
||||||
|
productName: string
|
||||||
|
workerName: string
|
||||||
|
shareQuantity: number | null
|
||||||
|
reason: string
|
||||||
|
status: 'pending' | 'approved' | 'rejected' | string
|
||||||
|
reviewNote: string
|
||||||
|
reviewedBy: string
|
||||||
|
createdAt: string
|
||||||
|
reviewedAt: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkerCancelRequestRisk = {
|
||||||
|
dailyUsed: number
|
||||||
|
dailyLimit: number
|
||||||
|
approvedTotal: number
|
||||||
|
nextPenaltyAt: number
|
||||||
|
remainingToPenalty: number
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkerFinanceConfig = {
|
export type WorkerFinanceConfig = {
|
||||||
depositUnfreezeDays: number
|
depositUnfreezeDays: number
|
||||||
recharge: {
|
recharge: {
|
||||||
@@ -125,6 +150,7 @@ export type WorkerPlatformNotificationEventKey =
|
|||||||
| 'acceptance_submitted'
|
| 'acceptance_submitted'
|
||||||
| 'acceptance_reminded'
|
| 'acceptance_reminded'
|
||||||
| 'material_required'
|
| 'material_required'
|
||||||
|
| 'cancel_requested'
|
||||||
|
|
||||||
export type WorkerPlatformNotificationConfig = {
|
export type WorkerPlatformNotificationConfig = {
|
||||||
events: Record<
|
events: Record<
|
||||||
|
|||||||
Reference in New Issue
Block a user