feat(worker-platform): add worker withdrawal blacklist and admin actions

This commit is contained in:
yml2213
2026-09-13 11:32:42 +08:00
parent 18ac95ca27
commit 061a5d897f
15 changed files with 405 additions and 7 deletions
@@ -0,0 +1,13 @@
-- 打手提现黑名单:
-- 1) is_withdrawal_blacklisted: 是否被列入提现黑名单(默认 false)
-- 2) withdrawal_blacklist_reason: 拉黑原因/备注
-- 3) withdrawal_blacklisted_at: 列入黑名单的时间
ALTER TABLE worker_users
ADD COLUMN IF NOT EXISTS is_withdrawal_blacklisted BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS withdrawal_blacklist_reason TEXT NULL,
ADD COLUMN IF NOT EXISTS withdrawal_blacklisted_at TIMESTAMPTZ NULL;
CREATE INDEX IF NOT EXISTS idx_worker_users_withdrawal_blacklisted
ON worker_users (is_withdrawal_blacklisted)
WHERE is_withdrawal_blacklisted = TRUE;
@@ -42,6 +42,9 @@ export type WorkerUserRow = {
total_settled_amount?: number
rename_cards?: number
last_renamed_at?: string | null
is_withdrawal_blacklisted?: boolean
withdrawal_blacklist_reason?: string | null
withdrawal_blacklisted_at?: string | null
}
export type WorkerUserStatistics = {
@@ -453,6 +456,8 @@ export type ListInput = {
/** 后台待验收筛选:普通单按接单人,拼单按参与打手等级。 */
workerLevelIds?: number[]
categoryId?: number
/** 后台打手列表按提现黑名单筛选。 */
isWithdrawalBlacklisted?: boolean | null | undefined
workerSharingId?: number
/** 大厅候选集合的总展示上限;筛选与分页在候选集合内执行。 */
hallCandidateLimit?: number
@@ -240,6 +240,7 @@ export async function listWorkerUsers({
workerType = '',
workerId = 0,
levelId = 0,
isWithdrawalBlacklisted,
}: ListInput = {}): Promise<{ items: WorkerUserRow[]; total: number }> {
const { whereClause, params } = buildWorkerUserWhere({
status,
@@ -247,6 +248,7 @@ export async function listWorkerUsers({
workerType,
workerId,
levelId,
isWithdrawalBlacklisted,
})
const totalResult = await query<{ total: number }>(
`SELECT COUNT(*)::int AS total FROM worker_users wu ${whereClause}`,
@@ -267,7 +269,10 @@ export async function listWorkerUsers({
/** 按筛选条件汇总打手,统计范围不受当前分页影响。 */
export async function getWorkerUserStatistics(
input: Pick<ListInput, 'status' | 'keyword' | 'workerType' | 'workerId' | 'levelId'> = {},
input: Pick<
ListInput,
'status' | 'keyword' | 'workerType' | 'workerId' | 'levelId' | 'isWithdrawalBlacklisted'
> = {},
): Promise<WorkerUserStatistics> {
const { whereClause, params } = buildWorkerUserWhere(input)
const statisticsResult = await query<{
@@ -373,13 +378,17 @@ async function getWorkerUserByIdWithClient(
return result.rows[0] || null
}
function buildWorkerUserWhere({
export function buildWorkerUserWhere({
status = '',
keyword = '',
workerType = '',
workerId = 0,
levelId = 0,
}: Pick<ListInput, 'status' | 'keyword' | 'workerType' | 'workerId' | 'levelId'>) {
isWithdrawalBlacklisted,
}: Pick<
ListInput,
'status' | 'keyword' | 'workerType' | 'workerId' | 'levelId' | 'isWithdrawalBlacklisted'
>) {
const filters: string[] = []
const params: unknown[] = []
if (status) {
@@ -404,12 +413,47 @@ function buildWorkerUserWhere({
params.push(levelId)
filters.push(`wu.level_id = $${params.length}`)
}
if (isWithdrawalBlacklisted !== undefined && isWithdrawalBlacklisted !== null) {
params.push(Boolean(isWithdrawalBlacklisted))
filters.push(`wu.is_withdrawal_blacklisted = $${params.length}`)
}
return {
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
params,
}
}
export async function updateWorkerWithdrawalBlacklist(input: {
workerId: number | string
isWithdrawalBlacklisted: boolean
reason: string
now: string
}): Promise<WorkerUserRow | null> {
const result = await query<{ id: number }>(
`
UPDATE worker_users
SET
is_withdrawal_blacklisted = $1,
withdrawal_blacklist_reason = $2,
withdrawal_blacklisted_at = $3,
updated_at = $4
WHERE id = $5
RETURNING id
`,
[
input.isWithdrawalBlacklisted,
input.isWithdrawalBlacklisted ? input.reason : null,
input.isWithdrawalBlacklisted ? input.now : null,
input.now,
Number(input.workerId),
],
)
if (!result.rows[0]) {
return null
}
return getWorkerUserById(input.workerId)
}
/** 消耗一张改名卡并更新昵称;条件更新保证并发下不会超发。 */
export async function consumeWorkerRenameCard(input: {
workerId: number
@@ -11,6 +11,7 @@ import {
reviewAdminWorkerUser,
saveAdminWorkerWithdrawalAccount,
setAdminWorkerPersonalDepositFreeAmount,
updateAdminWorkerWithdrawalBlacklist,
} from '../../../services/worker-platform/index.js'
import { createJsonHandler, requireAdminRoles } from '../session.js'
@@ -120,6 +121,26 @@ router.post(
),
)
router.post(
'/worker-platform/workers/:workerId/withdrawal-blacklist',
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) =>
updateAdminWorkerWithdrawalBlacklist(String(req.params.workerId || ''), req.body || {}),
{
successMessage: '打手提现黑名单状态已更新',
errorMessage: '更新打手提现黑名单状态失败',
scope: '[admin/worker-platform/workers/:workerId/withdrawal-blacklist]',
audit: (req, data) => ({
action: 'worker_withdrawal_blacklist_updated',
targetType: 'worker',
targetId: String(req.params.workerId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
},
),
)
router.post(
'/worker-platform/workers/:workerId/wallet-credit',
requireAdminRoles(['admin', 'operator']),
@@ -6,6 +6,7 @@ import {
revokeAllWorkerSessions,
updateWorkerPassword,
updateWorkerUser,
updateWorkerWithdrawalBlacklist,
} from '../../repositories/worker-platform/index.js'
import type { JsonObject } from '../../types/json.js'
import { createHttpError } from '../../utils/http.js'
@@ -31,12 +32,22 @@ export async function listAdminWorkerUsers(query: JsonObject = {}) {
const pageSize = normalizePageSize(query.pageSize)
const workerId = normalizeOptionalId(query.workerId ?? query.worker_id) || 0
const levelId = normalizeOptionalId(query.levelId ?? query.level_id) || 0
const rawWithdrawalBlacklisted =
query.isWithdrawalBlacklisted ?? query.is_withdrawal_blacklisted ?? query.withdrawalBlacklisted
const isWithdrawalBlacklisted =
rawWithdrawalBlacklisted !== undefined &&
rawWithdrawalBlacklisted !== null &&
String(rawWithdrawalBlacklisted).trim() !== ''
? normalizeBoolean(rawWithdrawalBlacklisted, false)
: undefined
const filters = {
workerId,
levelId,
status: String(query.status || '').trim(),
keyword: String(query.keyword || '').trim(),
workerType: String(query.workerType || query.worker_type || '').trim(),
isWithdrawalBlacklisted,
}
const includeStatistics = normalizeBoolean(
query.includeStatistics ?? query.include_statistics,
@@ -147,3 +158,23 @@ export async function resetAdminWorkerPassword(
await revokeAllWorkerSessions(worker.id, now)
return { worker: mapWorkerUser(updated || worker), reloginRequired: true }
}
export async function updateAdminWorkerWithdrawalBlacklist(
workerId: number | string,
payload: JsonObject = {},
) {
const worker = await getRequiredWorker(workerId)
const isWithdrawalBlacklisted = normalizeBoolean(
payload.isWithdrawalBlacklisted ?? payload.blacklisted,
false,
)
const reason = String(payload.reason || payload.blacklistReason || '').trim()
const now = nowIso()
const updated = await updateWorkerWithdrawalBlacklist({
workerId: worker.id,
isWithdrawalBlacklisted,
reason,
now,
})
return { worker: mapWorkerUser(updated || worker) }
}
@@ -102,6 +102,9 @@ export function mapWorkerUser(worker: WorkerUserRow) {
createdAt: worker.created_at,
updatedAt: worker.updated_at,
reviewedAt: worker.reviewed_at,
isWithdrawalBlacklisted: Boolean(worker.is_withdrawal_blacklisted),
withdrawalBlacklistReason: worker.withdrawal_blacklist_reason || '',
withdrawalBlacklistedAt: worker.withdrawal_blacklisted_at || null,
renameCards: Number(worker.rename_cards ?? 1),
lastRenamedAt: worker.last_renamed_at || null,
renameUnlockedAt: worker.last_renamed_at
@@ -110,6 +110,18 @@ export async function createWorkerWithdrawRequest(
) {
requireActiveWorkerSession(session)
const worker = await getRequiredWorker(session.workerId)
if (worker.is_withdrawal_blacklisted) {
const reason = worker.withdrawal_blacklist_reason?.trim()
throw createHttpError(
reason
? `您当前已被加入提现黑名单(原因:${reason}),无法发起提现,如有疑问请联系平台客服`
: '您当前已被加入提现黑名单,无法发起提现,如有疑问请联系平台客服',
{
statusCode: 403,
errorCode: 'worker_withdrawal_blacklisted',
},
)
}
const financeConfig = getWorkerFinanceConfig()
if (financeConfig.withdraw.enabled === false) {
throw createHttpError('当前提现入口暂未开放,请联系管理员', {
@@ -12,6 +12,7 @@ import type {
} from '../../repositories/worker-platform/index.js'
import {
buildWorkOrderWhere,
buildWorkerUserWhere,
resolveWorkOrderShareCancellationStatus,
resolveWorkOrderShareDepositAmount,
resolveWorkOrderSharingCancellationPlan,
@@ -26,6 +27,7 @@ import {
canWorkerAutoAcceptWholeOrder,
isWorkOrderSharingFilled,
isWorkerPasswordStrong,
mapWorkerUser,
normalizeWithdrawChannel,
normalizeRequirementFields,
normalizeWorkerFreezeReason,
@@ -1285,3 +1287,65 @@ test('资金流水映射展示关联的平台订单号', () => {
assert.equal(mapWalletLedger(ledger)?.relatedPlatformOrderId, '2623301845665410')
})
test('打手提现黑名单信息映射到前端结构', () => {
const worker = {
id: 99,
username: 'blacklisted_worker',
password_hash: 'hash',
display_name: '违规打手',
phone: '13800000000',
level_id: null,
status: 'active',
worker_type: 'external',
review_note: '',
frozen_reason: '',
frozen_at: null,
session_version: 1,
created_at: '2026-09-01T00:00:00.000Z',
updated_at: '2026-09-02T00:00:00.000Z',
reviewed_at: null,
inviter_id: null,
invite_code: 'ABCDEFGH',
is_withdrawal_blacklisted: true,
withdrawal_blacklist_reason: '账号涉嫌刷单,暂停提现',
withdrawal_blacklisted_at: '2026-09-13T00:00:00.000Z',
} satisfies WorkerUserRow
const mapped = mapWorkerUser(worker)
assert.equal(mapped.isWithdrawalBlacklisted, true)
assert.equal(mapped.withdrawalBlacklistReason, '账号涉嫌刷单,暂停提现')
assert.equal(mapped.withdrawalBlacklistedAt, '2026-09-13T00:00:00.000Z')
const normalWorker = {
...worker,
is_withdrawal_blacklisted: false,
withdrawal_blacklist_reason: null,
withdrawal_blacklisted_at: null,
} satisfies WorkerUserRow
const normalMapped = mapWorkerUser(normalWorker)
assert.equal(normalMapped.isWithdrawalBlacklisted, false)
assert.equal(normalMapped.withdrawalBlacklistReason, '')
assert.equal(normalMapped.withdrawalBlacklistedAt, null)
})
test('打手查询条件支持提现黑名单筛选', () => {
const blacklistedWhere = buildWorkerUserWhere({
isWithdrawalBlacklisted: true,
})
assert.match(blacklistedWhere.whereClause, /wu\.is_withdrawal_blacklisted = \$1/)
assert.deepEqual(blacklistedWhere.params, [true])
const normalWhere = buildWorkerUserWhere({
isWithdrawalBlacklisted: false,
})
assert.match(normalWhere.whereClause, /wu\.is_withdrawal_blacklisted = \$1/)
assert.deepEqual(normalWhere.params, [false])
const allWhere = buildWorkerUserWhere({
isWithdrawalBlacklisted: undefined,
})
assert.equal(allWhere.whereClause, '')
assert.equal(allWhere.params.length, 0)
})