From 85fc2eaa7cbcdca983d12d4003e32e09f7ced9a5 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 10 Oct 2026 13:44:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(worker-platform):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E4=BA=BA=E5=B7=A5=E8=B0=83=E7=BA=A7=E9=94=81?= =?UTF-8?q?=E5=AE=9A=E7=AD=89=E7=BA=A7=E4=B8=8E=E8=A7=A3=E9=99=A4=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8D=87=E7=BA=A7=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 做了什么: 1. 新增数据库迁移 098_worker_level_lock.sql,在 worker_users 表增加 is_level_locked、level_locked_at、level_locked_reason、level_locked_by 字段; 2. 后端自动升级函数 maybeUpgradeWorkerLevelWithClient 和判定函数 shouldApplyAutomaticWorkerLevel 增加 is_level_locked 守卫,被锁定的打手跳过自动升级; 3. 后台打手审核调级服务 reviewAdminWorkerUser 支持等级锁定参数与变更记录,人工调级时自动写入 worker_level_logs 流水,并提供 updateAdminWorkerLevelLock 专用接口; 4. 前端打手列表等级列展示「已锁定」状态及 Tooltip 原因,调级弹窗支持「锁定自动升级」开关与原因输入,操作列提供「解除限制」快捷操作; 5. 打手详情抽屉补充展示等级锁定说明。 - 为什么: 解决打手被人工调整等级(如 VIP5 调至 VIP4)后,因历史累计完成单数满足门槛导致后续结算再次自动升级回高等级的问题,并支持后续随时解除限制恢复自动升级。 - 验证方式: 1. 后端新增 shouldApplyAutomaticWorkerLevel 与 mapWorkerUser 单元测试,运行 npm run check(670 测试全过,类型与 SQL guard 验证通过); 2. 前端新增 worker-level-lock.test.ts 单元测试,运行 npm run check(130 测试全过,typecheck 与 build 验证通过)。 --- .../db/migrations/098_worker_level_lock.sql | 12 +++ .../repositories/worker-platform-repo.test.ts | 23 ++++ .../src/repositories/worker-platform/types.ts | 4 + .../worker-platform/worker-ranking-repo.ts | 7 +- .../worker-platform/worker-repo.ts | 16 ++- .../routes/admin/worker-platform/workers.ts | 32 +++++- .../services/worker-platform/admin-service.ts | 1 + .../admin-worker-user-service.ts | 100 +++++++++++++++++- .../worker-platform/worker-finance-mappers.ts | 4 + .../worker-platform-service.test.ts | 46 ++++++++ .../pages/admin/panels/WorkerDetailDrawer.tsx | 18 +++- .../src/pages/admin/panels/WorkersPanel.tsx | 87 ++++++++++++++- .../src/services/admin/worker-platform.ts | 15 +++ apps/frontend/src/types/worker-platform.ts | 8 ++ apps/frontend/tests/worker-level-lock.test.ts | 33 ++++++ 15 files changed, 395 insertions(+), 11 deletions(-) create mode 100644 apps/backend/src/db/migrations/098_worker_level_lock.sql create mode 100644 apps/frontend/tests/worker-level-lock.test.ts diff --git a/apps/backend/src/db/migrations/098_worker_level_lock.sql b/apps/backend/src/db/migrations/098_worker_level_lock.sql new file mode 100644 index 00000000..025cc77d --- /dev/null +++ b/apps/backend/src/db/migrations/098_worker_level_lock.sql @@ -0,0 +1,12 @@ +-- 098_worker_level_lock.sql —— 打手等级人工锁定与自动升级限制。 + +ALTER TABLE worker_users + ADD COLUMN IF NOT EXISTS is_level_locked BOOLEAN NOT NULL DEFAULT FALSE, + ADD COLUMN IF NOT EXISTS level_locked_at TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS level_locked_reason TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS level_locked_by TEXT NOT NULL DEFAULT ''; + +COMMENT ON COLUMN worker_users.is_level_locked IS '是否锁定打手等级,锁定后禁止自动升级'; +COMMENT ON COLUMN worker_users.level_locked_at IS '等级锁定时间'; +COMMENT ON COLUMN worker_users.level_locked_reason IS '等级锁定原因/调级备注'; +COMMENT ON COLUMN worker_users.level_locked_by IS '锁定操作人'; diff --git a/apps/backend/src/repositories/worker-platform-repo.test.ts b/apps/backend/src/repositories/worker-platform-repo.test.ts index 1a9ffa70..024016a5 100644 --- a/apps/backend/src/repositories/worker-platform-repo.test.ts +++ b/apps/backend/src/repositories/worker-platform-repo.test.ts @@ -60,6 +60,29 @@ test('automatic worker level changes only move to a higher threshold', () => { ) }) +test('automatic worker level changes are rejected when isLevelLocked is true', () => { + assert.equal( + shouldApplyAutomaticWorkerLevel({ + currentLevelId: 4, + currentThreshold: 50, + candidateLevelId: 5, + candidateThreshold: 100, + isLevelLocked: true, + }), + false, + ) + assert.equal( + shouldApplyAutomaticWorkerLevel({ + currentLevelId: 4, + currentThreshold: 50, + candidateLevelId: 5, + candidateThreshold: 100, + isLevelLocked: false, + }), + true, + ) +}) + test('指定打手的运营状态统计保留可见范围和拼单汇总关联', () => { const query = buildWorkOrderWhere({ workerSharingId: 3, diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 9440e152..6f1bc1e4 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -47,6 +47,10 @@ export type WorkerUserRow = { withdrawal_blacklisted_at?: string | null can_consign?: boolean consignment_activated_at?: string | null + is_level_locked?: boolean + level_locked_at?: string | null + level_locked_reason?: string + level_locked_by?: string } export type WorkerUserStatistics = { diff --git a/apps/backend/src/repositories/worker-platform/worker-ranking-repo.ts b/apps/backend/src/repositories/worker-platform/worker-ranking-repo.ts index 14d57e18..8b44282e 100644 --- a/apps/backend/src/repositories/worker-platform/worker-ranking-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-ranking-repo.ts @@ -141,6 +141,7 @@ export async function maybeUpgradeWorkerLevelWithClient( const current = await client.query<{ id: number level_id: number | null + is_level_locked: boolean level_penalty_accepted_count: number upgrade_threshold: number | null }>( @@ -148,6 +149,7 @@ export async function maybeUpgradeWorkerLevelWithClient( SELECT wu.id, wu.level_id, + COALESCE(wu.is_level_locked, FALSE) AS is_level_locked, wu.level_penalty_accepted_count, COALESCE((wl.permission_json->>'upgradeThreshold')::int, 0) AS upgrade_threshold FROM worker_users wu @@ -159,7 +161,7 @@ export async function maybeUpgradeWorkerLevelWithClient( [workerId], ) const currentRow = current.rows[0] - if (!currentRow) return null + if (!currentRow || currentRow.is_level_locked) return null const acceptedCount = await countWorkerAcceptedOrdersWithClient(client, workerId) const effectiveAcceptedCount = Math.max( @@ -191,6 +193,7 @@ export async function maybeUpgradeWorkerLevelWithClient( currentThreshold: Number(currentRow.upgrade_threshold || 0), candidateLevelId: nextLevelId, candidateThreshold: Number(nextLevel?.upgrade_threshold || 0), + isLevelLocked: currentRow.is_level_locked, }) ) { return null @@ -223,7 +226,9 @@ export function shouldApplyAutomaticWorkerLevel(input: { currentThreshold: number candidateLevelId: number candidateThreshold: number + isLevelLocked?: boolean }) { + if (input.isLevelLocked) return false if (!input.currentLevelId) return true if (Number(input.currentLevelId) === Number(input.candidateLevelId)) return false return Number(input.candidateThreshold) > Number(input.currentThreshold) diff --git a/apps/backend/src/repositories/worker-platform/worker-repo.ts b/apps/backend/src/repositories/worker-platform/worker-repo.ts index 27ef60b9..b8735e4e 100644 --- a/apps/backend/src/repositories/worker-platform/worker-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-repo.ts @@ -149,6 +149,10 @@ export async function updateWorkerUser( | 'session_version' | 'updated_at' | 'reviewed_at' + | 'is_level_locked' + | 'level_locked_at' + | 'level_locked_reason' + | 'level_locked_by' > >, ): Promise { @@ -169,8 +173,12 @@ export async function updateWorkerUser( personal_deposit_free_amount = $7, session_version = $8, reviewed_at = $9, - updated_at = $10 - WHERE id = $11 + updated_at = $10, + is_level_locked = $11, + level_locked_at = $12, + level_locked_reason = $13, + level_locked_by = $14 + WHERE id = $15 `, [ next.status, @@ -183,6 +191,10 @@ export async function updateWorkerUser( Number(next.session_version || 1), next.reviewed_at || null, next.updated_at, + Boolean(next.is_level_locked), + next.level_locked_at || null, + next.level_locked_reason || '', + next.level_locked_by || '', Number(workerId), ], ) diff --git a/apps/backend/src/routes/admin/worker-platform/workers.ts b/apps/backend/src/routes/admin/worker-platform/workers.ts index c782b99c..baa09992 100644 --- a/apps/backend/src/routes/admin/worker-platform/workers.ts +++ b/apps/backend/src/routes/admin/worker-platform/workers.ts @@ -9,6 +9,7 @@ import { listAdminWorkerWithdrawalAccounts, resetAdminWorkerPassword, reviewAdminWorkerUser, + updateAdminWorkerLevelLock, saveAdminWorkerWithdrawalAccount, setAdminWorkerPersonalDepositFreeAmount, updateAdminWorkerWithdrawalBlacklist, @@ -68,7 +69,12 @@ router.post( '/worker-platform/workers/:workerId/review', requireAdminRoles(['admin', 'operator']), createJsonHandler( - (req) => reviewAdminWorkerUser(String(req.params.workerId || ''), req.body || {}), + (req) => + reviewAdminWorkerUser( + String(req.params.workerId || ''), + req.body || {}, + req.adminSession?.username || '', + ), { successMessage: '打手账号状态已更新', errorMessage: '更新打手账号状态失败', @@ -83,6 +89,30 @@ router.post( ), ) +router.post( + '/worker-platform/workers/:workerId/level-lock', + requireAdminRoles(['admin', 'operator']), + createJsonHandler( + (req) => + updateAdminWorkerLevelLock( + String(req.params.workerId || ''), + req.body || {}, + req.adminSession?.username || '', + ), + { + successMessage: '打手等级锁定状态已更新', + errorMessage: '更新打手等级锁定状态失败', + scope: '[admin/worker-platform/workers/:workerId/level-lock]', + audit: (req, data) => ({ + action: 'worker_level_lock_updated', + targetType: 'worker', + targetId: String(req.params.workerId || ''), + data: data && typeof data === 'object' ? (data as Record) : {}, + }), + }, + ), +) + router.post( '/worker-platform/workers/:workerId/reset-password', requireAdminRoles(['admin', 'operator']), diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index 6bf89105..a8366b8e 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -47,6 +47,7 @@ export { listAdminWorkerUsers, resetAdminWorkerPassword, reviewAdminWorkerUser, + updateAdminWorkerLevelLock, setAdminWorkerPersonalDepositFreeAmount, } from './admin-worker-user-service.js' export { syncWorkerOrdersForSourceOrder } from './sync-worker-orders-service.js' diff --git a/apps/backend/src/services/worker-platform/admin-worker-user-service.ts b/apps/backend/src/services/worker-platform/admin-worker-user-service.ts index 18daf92f..40436a95 100644 --- a/apps/backend/src/services/worker-platform/admin-worker-user-service.ts +++ b/apps/backend/src/services/worker-platform/admin-worker-user-service.ts @@ -1,3 +1,4 @@ +import { query } from '../../db/client.js' import { countTimeoutEventsByWorkerIds, getWorkerUserStatistics, @@ -70,7 +71,11 @@ export async function listAdminWorkerUsers(query: JsonObject = {}) { } } -export async function reviewAdminWorkerUser(workerId: number | string, payload: JsonObject = {}) { +export async function reviewAdminWorkerUser( + workerId: number | string, + payload: JsonObject = {}, + operatorUsername: string = '', +) { const worker = await getRequiredWorker(workerId) const status = normalizeReviewStatus(payload.status) const freezeReason = @@ -80,6 +85,7 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload: const workerType = normalizeWorkerType(payload.workerType ?? payload.worker_type) const requestedLevelId = normalizeOptionalId(payload.levelId) let levelId = worker.level_id || null + let isLevelChanged = false if (requestedLevelId) { const level = await getWorkerLevelById(requestedLevelId) if (!level) { @@ -89,11 +95,51 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload: }) } levelId = Number(level.id) + if (Number(worker.level_id) !== levelId) { + isLevelChanged = true + } } if (!levelId) { levelId = (await ensureWorkerPlatformDefaults()).level?.id || null } const now = nowIso() + + // 处理等级锁定逻辑 + const rawIsLevelLocked = payload.isLevelLocked ?? payload.is_level_locked + let nextIsLevelLocked: boolean + if (rawIsLevelLocked !== undefined && rawIsLevelLocked !== null) { + nextIsLevelLocked = Boolean(rawIsLevelLocked) + } else if (isLevelChanged) { + // 若人工调整了等级但未显式指定锁,默认锁定(防止被自动升级冲掉) + nextIsLevelLocked = true + } else { + nextIsLevelLocked = Boolean(worker.is_level_locked) + } + + const isLockStatusChanged = nextIsLevelLocked !== Boolean(worker.is_level_locked) + let nextLockedAt = worker.level_locked_at || null + let nextLockedReason = worker.level_locked_reason || '' + let nextLockedBy = worker.level_locked_by || '' + + if (nextIsLevelLocked) { + if (isLockStatusChanged || isLevelChanged || !nextLockedAt) { + nextLockedAt = now + nextLockedReason = String( + payload.levelLockedReason ?? + payload.lockReason ?? + payload.reviewNote ?? + worker.level_locked_reason ?? + '人工调整等级锁定', + ).trim() + nextLockedBy = + operatorUsername || String(payload.operator || worker.level_locked_by || '').trim() + } + } else { + nextLockedAt = null + nextLockedReason = '' + nextLockedBy = '' + } + const updated = await updateWorkerUser(worker.id, { status, worker_type: workerType, @@ -107,7 +153,43 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload: session_version: normalizeSessionVersion(worker.session_version) + 1, reviewed_at: now, updated_at: now, + is_level_locked: nextIsLevelLocked, + level_locked_at: nextLockedAt, + level_locked_reason: nextLockedReason, + level_locked_by: nextLockedBy, }) + + // 如果等级发生了变动,或者锁定状态发生了变动,写入 worker_level_logs + if (isLevelChanged || isLockStatusChanged) { + const logReason = isLevelChanged + ? 'admin_adjust' + : nextIsLevelLocked + ? 'admin_lock' + : 'admin_unlock' + try { + await query( + `INSERT INTO worker_level_logs ( + worker_id, from_level_id, to_level_id, reason, payload_json, created_at + ) VALUES ($1, $2, $3, $4, $5::jsonb, $6)`, + [ + worker.id, + worker.level_id || null, + levelId, + logReason, + JSON.stringify({ + operator: operatorUsername || payload.operator || '', + isLevelLocked: nextIsLevelLocked, + reason: nextLockedReason, + note: payload.reviewNote || '', + }), + now, + ], + ) + } catch { + // 日志记录失败不阻断主流程 + } + } + await revokeAllWorkerSessions(worker.id, now) // 必须同步释放内存里的风控状态。 @@ -120,6 +202,22 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload: return { worker: mapWorkerUser(updated || worker) } } +export async function updateAdminWorkerLevelLock( + workerId: number | string, + payload: JsonObject = {}, + operatorUsername: string = '', +) { + const isLevelLocked = Boolean(payload.isLevelLocked ?? payload.is_level_locked) + return reviewAdminWorkerUser( + workerId, + { + isLevelLocked, + levelLockedReason: payload.levelLockedReason ?? payload.lockReason, + }, + operatorUsername, + ) +} + /** 设置单个打手的追加免押额度,会与该打手等级的免押额度叠加。 */ export async function setAdminWorkerPersonalDepositFreeAmount( workerId: number | string, diff --git a/apps/backend/src/services/worker-platform/worker-finance-mappers.ts b/apps/backend/src/services/worker-platform/worker-finance-mappers.ts index d33cb6eb..c7531be3 100644 --- a/apps/backend/src/services/worker-platform/worker-finance-mappers.ts +++ b/apps/backend/src/services/worker-platform/worker-finance-mappers.ts @@ -112,6 +112,10 @@ export function mapWorkerUser(worker: WorkerUserRow) { : null, canConsign: Boolean(worker.can_consign), consignmentActivatedAt: worker.consignment_activated_at || null, + isLevelLocked: Boolean(worker.is_level_locked), + levelLockedAt: worker.level_locked_at || null, + levelLockedReason: worker.level_locked_reason || '', + levelLockedBy: worker.level_locked_by || '', } } diff --git a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts index 5efee579..5fbe6543 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts @@ -2119,6 +2119,52 @@ test('打手提现黑名单信息映射到前端结构', () => { assert.equal(normalMapped.withdrawalBlacklistedAt, null) }) +test('打手等级锁定信息正确映射到前端结构', () => { + const lockedWorker = { + id: 1, + username: 'worker1', + password_hash: 'hash', + display_name: '打手1', + phone: '13800000000', + level_id: 4, + 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-01T00:00:00.000Z', + reviewed_at: null, + inviter_id: null, + invite_code: 'INVITE1', + is_level_locked: true, + level_locked_at: '2026-10-10T10:00:00.000Z', + level_locked_reason: '人工调整为VIP4', + level_locked_by: 'admin_test', + } satisfies WorkerUserRow + + const mapped = mapWorkerUser(lockedWorker) + assert.equal(mapped.isLevelLocked, true) + assert.equal(mapped.levelLockedAt, '2026-10-10T10:00:00.000Z') + assert.equal(mapped.levelLockedReason, '人工调整为VIP4') + assert.equal(mapped.levelLockedBy, 'admin_test') + + const unlockedWorker = { + ...lockedWorker, + is_level_locked: false, + level_locked_at: null, + level_locked_reason: '', + level_locked_by: '', + } satisfies WorkerUserRow + + const unlockedMapped = mapWorkerUser(unlockedWorker) + assert.equal(unlockedMapped.isLevelLocked, false) + assert.equal(unlockedMapped.levelLockedAt, null) + assert.equal(unlockedMapped.levelLockedReason, '') + assert.equal(unlockedMapped.levelLockedBy, '') +}) + test('打手查询条件支持提现黑名单筛选', () => { const blacklistedWhere = buildWorkerUserWhere({ isWithdrawalBlacklisted: true, diff --git a/apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx b/apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx index 4732f5c5..862a9890 100644 --- a/apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx @@ -153,7 +153,23 @@ function OverviewSection({ workerId }: { workerId: number }) { {worker.displayName || '-'} {worker.username} {worker.phone || '-'} - {worker.level?.name || '-'} + + + {worker.level?.name || '-'} + {worker.isLevelLocked ? 已锁定自动升级 : null} + + + {worker.isLevelLocked ? ( + + + {worker.levelLockedReason ? `原因: ${worker.levelLockedReason}` : '人工调级锁定'} + {worker.levelLockedBy ? ` · 操作人: ${worker.levelLockedBy}` : ''} + {worker.levelLockedAt + ? ` · 时间: ${formatAdminDateTime(worker.levelLockedAt)}` + : ''} + + + ) : null} {worker.inviter ? `${worker.inviter.displayName || worker.inviter.username}` : '-'} diff --git a/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx index b67588ea..9d245f67 100644 --- a/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx @@ -1,4 +1,4 @@ -import { ReloadOutlined, SearchOutlined } from '@ant-design/icons' +import { LockOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons' import { useQuery, useQueryClient } from '@tanstack/react-query' import { Alert, @@ -9,6 +9,7 @@ import { Input, InputNumber, Modal, + Popconfirm, Select, Space, Switch, @@ -29,6 +30,7 @@ import { fetchAdminWorkerWithdrawalAccounts, resetAdminWorkerPassword, reviewAdminWorkerUser, + updateAdminWorkerLevelLock, saveAdminWorkerFinanceConfig, saveAdminWorkerPersonalDepositFreeAmount, saveAdminWorkerWithdrawalAccount, @@ -149,7 +151,11 @@ export default function WorkersPanel() { const [loadingWithdrawalAccounts, setLoadingWithdrawalAccounts] = useState(false) const [savingWithdrawalAccount, setSavingWithdrawalAccount] = useState(false) const [creditForm] = Form.useForm() - const [levelForm] = Form.useForm<{ levelId?: number }>() + const [levelForm] = Form.useForm<{ + levelId?: number + isLevelLocked?: boolean + levelLockedReason?: string + }>() const [withdrawalAccountForm] = Form.useForm() const [resetPasswordForm] = Form.useForm<{ password: string }>() const [freezeForm] = Form.useForm() @@ -388,13 +394,19 @@ export default function WorkersPanel() { } } - async function submitLevel(values: { levelId?: number }) { + async function submitLevel(values: { + levelId?: number + isLevelLocked?: boolean + levelLockedReason?: string + }) { if (!levelWorker) return try { await reviewAdminWorkerUser(levelWorker.workerId, { status: levelWorker.status, levelId: Number(values.levelId || 0), reviewNote: levelWorker.reviewNote, + isLevelLocked: Boolean(values.isLevelLocked), + levelLockedReason: values.levelLockedReason?.trim() || undefined, }) message.success('打手等级已更新') setLevelWorker(null) @@ -405,6 +417,18 @@ export default function WorkersPanel() { } } + async function handleUnlockLevel(worker: WorkerUser) { + try { + await updateAdminWorkerLevelLock(worker.workerId, { + isLevelLocked: false, + }) + message.success(`已解除打手 ${worker.displayName || worker.username} 的升级限制`) + await refreshWorkers() + } catch (error) { + message.error(error instanceof Error ? error.message : '解除升级限制失败') + } + } + async function submitPersonalDepositFreeAmount(values: PersonalDepositFreeAmountFormValues) { if (!personalDepositFreeWorker) return try { @@ -605,7 +629,26 @@ export default function WorkersPanel() { ), }, - { title: '等级', width: 140, render: (_, row) => row.level?.name || '-' }, + { + title: '等级', + width: 150, + render: (_, row) => ( + + {row.level?.name || '-'} + {row.isLevelLocked ? ( + + }> + 已锁定 + + + ) : null} + + ), + }, { title: '免押额度', width: 280, @@ -738,11 +781,24 @@ export default function WorkersPanel() { setLevelWorker(row) levelForm.setFieldsValue({ levelId: row.level?.levelId, + isLevelLocked: row.isLevelLocked ?? true, + levelLockedReason: row.levelLockedReason || '', }) }} > 调等级 + {row.isLevelLocked ? ( + void handleUnlockLevel(row)} + > + + + ) : null}