feat(worker-platform): 增加寄卖白名单总开关,关闭后全员可发布

资金配置与打手列表可一键切换白名单模式;发布校验、打手端入口按总开关放行。同时收紧未开通判定、关权限确认和开通弹窗文案。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-21 14:23:00 +08:00
co-authored by Cursor
parent 937e51db6b
commit 9298b31b9b
17 changed files with 342 additions and 56 deletions
@@ -0,0 +1,26 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { canWorkerPublishConsignment } from './consignment-whitelist.js'
test('白名单关闭时所有打手都能发布寄卖', () => {
assert.equal(
canWorkerPublishConsignment({ whitelistEnabled: false, workerCanConsign: false }),
true,
)
assert.equal(
canWorkerPublishConsignment({ whitelistEnabled: false, workerCanConsign: true }),
true,
)
})
test('白名单开启时仅开通权限的打手能发布', () => {
assert.equal(
canWorkerPublishConsignment({ whitelistEnabled: true, workerCanConsign: false }),
false,
)
assert.equal(
canWorkerPublishConsignment({ whitelistEnabled: true, workerCanConsign: true }),
true,
)
})
@@ -0,0 +1,12 @@
/**
* 寄卖白名单总开关。
*
* 开启:只有 worker.can_consign 的打手能发布新寄卖。
* 关闭:所有打手都能发布;打手身上的白名单标记仍保留,方便再次开启。
*/
export function canWorkerPublishConsignment(input: {
whitelistEnabled: boolean
workerCanConsign: boolean
}): boolean {
return !input.whitelistEnabled || input.workerCanConsign
}
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'
import test from 'node:test'
import { mapWorkerUser } from './worker-finance-mappers.js'
import { parseAdminWorkerConsignmentPermission } from './admin-worker-user-service.js'
import type { WorkerUserRow } from '../../repositories/worker-platform/types.js'
test('mapWorkerUser 准确回显打手寄卖白名单权限与激活时间', () => {
@@ -48,3 +49,12 @@ test('管理员打手管理路由注册了寄卖白名单权限更新接口', as
'路由必须调用 updateAdminWorkerConsignmentPermission',
)
})
test('更新寄卖权限必须显式传开关,空 body 不得默认关闭', () => {
assert.throws(
() => parseAdminWorkerConsignmentPermission({}),
(error: unknown) => (error as { errorCode?: string }).errorCode === 'can_consign_required',
)
assert.equal(parseAdminWorkerConsignmentPermission({ canConsign: true }), true)
assert.equal(parseAdminWorkerConsignmentPermission({ enabled: false }), false)
})
@@ -180,12 +180,23 @@ export async function updateAdminWorkerWithdrawalBlacklist(
return { worker: mapWorkerUser(updated || worker) }
}
export function parseAdminWorkerConsignmentPermission(payload: JsonObject = {}): boolean {
const raw = payload.canConsign ?? payload.enabled
if (raw === undefined || raw === null || raw === '') {
throw createHttpError('请明确指定寄卖权限开关', {
statusCode: 400,
errorCode: 'can_consign_required',
})
}
return normalizeBoolean(raw, false)
}
export async function updateAdminWorkerConsignmentPermission(
workerId: number | string,
payload: JsonObject = {},
) {
const worker = await getRequiredWorker(workerId)
const canConsign = normalizeBoolean(payload.canConsign ?? payload.enabled, false)
const canConsign = parseAdminWorkerConsignmentPermission(payload)
const now = nowIso()
const updated = await updateWorkerConsignmentPermission({
workerId: worker.id,
@@ -87,8 +87,8 @@ test('寄卖提报硬校验:璀璨数不能超过钻石等级(1钻最多1璀
test('未开通寄卖白名单权限的打手提报寄卖时被强校验拦截', async () => {
const serviceSource = await readServiceSource()
assert.ok(
serviceSource.includes('!worker.can_consign'),
'服务端必须拦截未开通寄卖权限的打手',
serviceSource.includes('canWorkerPublishConsignment('),
'服务端必须按白名单总开关决定是否拦截未开通打手',
)
assert.ok(
serviceSource.includes("errorCode: 'consignment_permission_required'"),
@@ -12,6 +12,7 @@ import { listEnabledWorkProductRulesForSuggestion } from '../../repositories/wor
import { listWorkOrdersByIds } from '../../repositories/worker-platform/work-order-query-repo.js'
import type { WorkOrderRow } from '../../repositories/worker-platform/types.js'
import { deriveConsignmentItemStage } from '../../domain/consignment-item-stage.js'
import { canWorkerPublishConsignment } from '../../domain/consignment-whitelist.js'
import { suggestConsignmentPriceFromRules } from './consignment-matching-service.js'
import { hasInternalConsignmentFulfillment } from '../../repositories/worker-platform/work-order-gift-repo.js'
import type { JsonObject } from '../../types/json.js'
@@ -20,7 +21,10 @@ import { generateConsignmentSubmissionNo } from '../../utils/random.js'
import { nowIso } from '../../utils/time.js'
import { refreshStoredFileUrl } from '../file-storage/file-storage-service.js'
import { publishWorkerWalletRealtimeChange } from '../realtime/realtime-event-service.js'
import { getConsignmentPublishDepositFen as getConfiguredConsignmentPublishDepositFen } from './worker-finance-config-service.js'
import {
getConsignmentPublishDepositFen as getConfiguredConsignmentPublishDepositFen,
getWorkerFinanceConfig,
} from './worker-finance-config-service.js'
import { confirmAdminWorkOrderFriendAdded } from './admin-work-order-gift-service.js'
import { submitWorkerOrderAcceptance } from './worker-acceptance-service.js'
import type { WorkerSession } from './worker-session-context-service.js'
@@ -144,8 +148,13 @@ export async function publishWorkerConsignment(workerId: number, payload: JsonOb
if (!worker) {
throw createHttpError('打手账户不存在', { statusCode: 404, errorCode: 'worker_not_found' })
}
if (!worker.can_consign) {
throw createHttpError('您暂未开通寄卖权限,请联系客服开通', {
if (
!canWorkerPublishConsignment({
whitelistEnabled: getWorkerFinanceConfig().consignmentWhitelistEnabled,
workerCanConsign: Boolean(worker.can_consign),
})
) {
throw createHttpError('您暂未开通寄卖权限,请联系客服申请开通', {
statusCode: 403,
errorCode: 'consignment_permission_required',
})
@@ -12,6 +12,11 @@ export type WorkerFinanceConfig = {
rewardUnfreezeEnabled: boolean
/** 苏尔南寄卖发布所需履约押金(分),默认 2000 = ¥20 */
consignmentPublishDepositFen: number
/**
* 寄卖白名单总开关。开启后仅 can_consign 打手可发布;关闭后所有打手可发布。
* 缺省视为开启,保持当前白名单行为。
*/
consignmentWhitelistEnabled: boolean
recharge: {
enabled: boolean
channelName: string
@@ -71,6 +76,9 @@ export function normalizeWorkerFinanceConfig(rawValue: unknown): WorkerFinanceCo
0,
1_000_000,
),
consignmentWhitelistEnabled:
source.consignmentWhitelistEnabled !== false &&
source.consignment_whitelist_enabled !== false,
recharge: {
enabled: typeof recharge.enabled === 'boolean' ? recharge.enabled : true,
channelName: String(recharge.channelName || recharge.channel_name || '').trim(),
@@ -97,6 +105,7 @@ export function createDefaultWorkerFinanceConfig(): WorkerFinanceConfig {
depositUnfreezeDays: 3,
rewardUnfreezeEnabled: true,
consignmentPublishDepositFen: 2000,
consignmentWhitelistEnabled: true,
recharge: {
enabled: true,
channelName: '',
@@ -120,6 +129,7 @@ function refreshWorkerFinanceConfig(config: WorkerFinanceConfig): WorkerFinanceC
depositUnfreezeDays: config.depositUnfreezeDays,
rewardUnfreezeEnabled: config.rewardUnfreezeEnabled,
consignmentPublishDepositFen: config.consignmentPublishDepositFen,
consignmentWhitelistEnabled: config.consignmentWhitelistEnabled,
recharge: {
...config.recharge,
qrCodeImage: config.recharge.qrCodeImage
@@ -402,6 +402,19 @@ test('寄卖发布押金配置保留显式零值并回退非法值', () => {
)
})
test('寄卖白名单总开关缺省开启,显式 false 才全员开放', () => {
assert.equal(normalizeWorkerFinanceConfig({}).consignmentWhitelistEnabled, true)
assert.equal(
normalizeWorkerFinanceConfig({ consignmentWhitelistEnabled: false })
.consignmentWhitelistEnabled,
false,
)
assert.equal(
normalizeWorkerFinanceConfig({ consignmentWhitelistEnabled: true }).consignmentWhitelistEnabled,
true,
)
})
test('寄卖押金释放先锁钱包再读流水,冻结额不足时拒绝部分退款', async () => {
const statements: string[] = []
const client = {