接单平台新增 VIP 等级体系与邀请绑定

- 等级按累计验收通过工单数自动升级(阈值后台可配),VIP1-5 默认配置 10/30/50/100
- 存量打手统一归 VIP1,原 regular 等级停用
- 注册支持邀请码绑定邀请人,每人生成唯一邀请码
- 新增 worker_level_logs 升级流水表
- 个人中心展示邀请码与升级进度,管理端等级表单支持升级阈值配置
This commit is contained in:
yml2213
2026-08-01 14:50:08 +08:00
parent 6e635b2499
commit 51d72cc283
9 changed files with 353 additions and 21 deletions
@@ -0,0 +1,55 @@
-- 007_worker_vip_levels_and_invite.sql —— VIP 等级体系与邀请绑定。
--
-- 说明:
-- 1. 等级升级按累计验收通过(accepted)工单数,阈值存 permission_json.upgradeThreshold,后台可配;
-- 2. 每个打手拥有唯一邀请码,注册时可填邀请码绑定邀请人(inviter_id);
-- 3. 存量打手统一归 VIP1,原 regular 等级停用;
-- 4. 佣金结算暂缓(规则未定),本迁移只落邀请关系,后续按规则补佣金。
-- 1) worker_users 增加邀请字段
ALTER TABLE worker_users ADD COLUMN IF NOT EXISTS inviter_id BIGINT REFERENCES worker_users(id) ON DELETE SET NULL;
ALTER TABLE worker_users ADD COLUMN IF NOT EXISTS invite_code TEXT;
-- 2) 存量用户填充唯一邀请码(8 位大写字母数字)
UPDATE worker_users
SET invite_code = UPPER(SUBSTRING(MD5(random()::text || id::text) FROM 1 FOR 8))
WHERE invite_code IS NULL;
ALTER TABLE worker_users ALTER COLUMN invite_code SET NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_worker_users_invite_code ON worker_users(invite_code);
CREATE INDEX IF NOT EXISTS idx_worker_users_inviter ON worker_users(inviter_id);
-- 3) seed VIP1-5 等级(level_key 唯一,重复执行不覆盖)
INSERT INTO worker_levels (level_key, name, sort_order, status, permission_json, created_at, updated_at) VALUES
('vip1', 'VIP1', 10, 'active', '{"depositFreeAmount":0,"maxActiveOrders":1,"upgradeThreshold":0}', NOW(), NOW()),
('vip2', 'VIP2', 20, 'active', '{"depositFreeAmount":0,"maxActiveOrders":3,"upgradeThreshold":10}', NOW(), NOW()),
('vip3', 'VIP3', 30, 'active', '{"depositFreeAmount":0,"maxActiveOrders":10,"upgradeThreshold":30}', NOW(), NOW()),
('vip4', 'VIP4', 40, 'active', '{"depositFreeAmount":0,"maxActiveOrders":50,"upgradeThreshold":50}', NOW(), NOW()),
('vip5', 'VIP5', 50, 'active', '{"depositFreeAmount":0,"maxActiveOrders":100,"upgradeThreshold":100}', NOW(), NOW())
ON CONFLICT (level_key) DO NOTHING;
-- 4) 存量打手统一归 VIP1,原 regular 等级停用
UPDATE worker_levels SET status = 'disabled' WHERE level_key = 'regular';
UPDATE worker_users
SET level_id = (SELECT id FROM worker_levels WHERE level_key = 'vip1')
WHERE level_id IS NULL
OR level_id = (SELECT id FROM worker_levels WHERE level_key = 'regular');
-- 5) 打手等级升级记录
CREATE TABLE IF NOT EXISTS worker_level_logs (
id BIGSERIAL PRIMARY KEY,
worker_id BIGINT NOT NULL REFERENCES worker_users(id) ON DELETE CASCADE,
from_level_id BIGINT REFERENCES worker_levels(id) ON DELETE SET NULL,
to_level_id BIGINT NOT NULL REFERENCES worker_levels(id) ON DELETE SET NULL,
reason TEXT NOT NULL DEFAULT 'auto',
payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_worker_level_logs_worker_created
ON worker_level_logs(worker_id, created_at DESC);
COMMENT ON COLUMN worker_users.inviter_id IS '邀请人(注册时通过邀请码绑定)';
COMMENT ON COLUMN worker_users.invite_code IS '本人专属邀请码,注册/邀请他人时使用';
COMMENT ON TABLE worker_level_logs IS '打手等级升级流水';
COMMENT ON COLUMN worker_level_logs.reason IS '升级原因:auto 自动升级 / admin 后台调整';
@@ -26,6 +26,10 @@ export type WorkerUserRow = {
created_at: string
updated_at: string
reviewed_at: string | null
inviter_id: number | null
invite_code: string
inviter_username?: string
inviter_display_name?: string
level_key?: string
level_name?: string
level_permission_json?: string | Record<string, unknown>
@@ -192,6 +196,8 @@ type CreateWorkerInput = {
phone: string
levelId: number | null
status: string
inviterId?: number | null
inviteCode: string
now: string
}
@@ -218,12 +224,15 @@ const WORKER_USER_SELECT = `
wl.level_key,
wl.name AS level_name,
wl.permission_json AS level_permission_json,
inv.username AS inviter_username,
inv.display_name AS inviter_display_name,
ww.available_amount,
ww.frozen_deposit_amount,
ww.total_credited_amount,
ww.total_settled_amount
FROM worker_users wu
LEFT JOIN worker_levels wl ON wl.id = wu.level_id
LEFT JOIN worker_users inv ON inv.id = wu.inviter_id
LEFT JOIN worker_wallets ww ON ww.worker_id = wu.id
`
@@ -325,6 +334,16 @@ export async function getWorkerUserByUsername(username: string): Promise<WorkerU
return result.rows[0] || null
}
export async function getWorkerUserByInviteCode(
inviteCode: string,
): Promise<WorkerUserRow | null> {
const result = await query<WorkerUserRow>(
`${WORKER_USER_SELECT} WHERE wu.invite_code = $1 LIMIT 1`,
[String(inviteCode || '').trim().toUpperCase()],
)
return result.rows[0] || null
}
export async function getWorkerUserById(workerId: number | string): Promise<WorkerUserRow | null> {
const result = await query<WorkerUserRow>(`${WORKER_USER_SELECT} WHERE wu.id = $1 LIMIT 1`, [
Number(workerId),
@@ -338,8 +357,9 @@ export async function createWorkerUser(input: CreateWorkerInput): Promise<Worker
`
INSERT INTO worker_users (
username, password_hash, display_name, phone, level_id, status,
inviter_id, invite_code,
created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id
`,
[
@@ -349,6 +369,8 @@ export async function createWorkerUser(input: CreateWorkerInput): Promise<Worker
input.phone,
input.levelId,
input.status,
input.inviterId || null,
input.inviteCode,
input.now,
input.now,
],
@@ -824,6 +846,64 @@ export async function countWorkerAcceptedOrders(workerId: number | string): Prom
return Number(result.rows[0]?.total || 0)
}
export async function maybeUpgradeWorkerLevelWithClient(
client: PoolClient,
workerId: number,
now: string,
): Promise<WorkerUserRow | null> {
const acceptedResult = await client.query<{ total: number }>(
`
SELECT COUNT(*)::int AS total
FROM work_orders
WHERE assigned_worker_id = $1
AND status = 'accepted'
`,
[workerId],
)
const acceptedCount = Number(acceptedResult.rows[0]?.total || 0)
const nextLevelResult = await client.query<{ id: number }>(
`
SELECT id
FROM worker_levels
WHERE status = 'active'
AND COALESCE((permission_json->>'upgradeThreshold')::int, 0) <= $1
ORDER BY COALESCE((permission_json->>'upgradeThreshold')::int, 0) DESC, sort_order ASC
LIMIT 1
`,
[acceptedCount],
)
const nextLevelId = Number(nextLevelResult.rows[0]?.id || 0)
if (!nextLevelId) return null
const current = await client.query<{ id: number; level_id: number | null }>(
'SELECT id, level_id FROM worker_users WHERE id = $1 LIMIT 1',
[workerId],
)
const currentRow = current.rows[0]
if (!currentRow || Number(currentRow.level_id || 0) === nextLevelId) return null
await client.query(
'UPDATE worker_users SET level_id = $1, updated_at = $2 WHERE id = $3',
[nextLevelId, now, 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, 'auto', $4::jsonb, $5)
`,
[
workerId,
currentRow.level_id || null,
nextLevelId,
JSON.stringify({ acceptedCount }),
now,
],
)
return getWorkerUserByIdWithClient(client, workerId)
}
export async function getWorkCategoryByKey(categoryKey: string): Promise<WorkCategoryRow | null> {
const result = await query<WorkCategoryRow>(
'SELECT * FROM work_categories WHERE category_key = $1 LIMIT 1',
@@ -1304,6 +1384,10 @@ export async function acceptWorkOrderAndSettle(input: {
now: input.now,
})
if (workerId > 0) {
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
}
return getWorkOrderByIdWithClient(client, input.workOrderId)
})
}
@@ -16,6 +16,8 @@ import {
getWorkerFinanceRequestById,
getWorkerFinanceRequestSummary,
getWorkerLevelById,
getWorkerLevelByKey,
getWorkerUserByInviteCode,
incrementWorkerSessionVersion,
getWorkOrderById,
getWorkOrderByOrderItemId,
@@ -73,9 +75,12 @@ export type WorkerSession = {
sessionVersion: number
}
const DEFAULT_LEVEL_KEY = 'regular'
const DEFAULT_LEVEL_KEY = 'vip1'
const DEFAULT_LEVEL_NAME = 'VIP1'
const DEFAULT_CATEGORY_KEY = 'default'
const DEFAULT_DEPOSIT_THRESHOLD_AMOUNT = 20_000
const INVITE_CODE_LENGTH = 8
const INVITE_CODE_RETRY_LIMIT = 5
const IGNORABLE_WORKER_AUTH_ERROR_CODES = new Set([
'worker_auth_required',
'worker_auth_invalid',
@@ -86,17 +91,21 @@ const IGNORABLE_WORKER_AUTH_ERROR_CODES = new Set([
export async function ensureWorkerPlatformDefaults() {
const now = nowIso()
const level = await upsertWorkerLevel({
const existingLevel = await getWorkerLevelByKey(DEFAULT_LEVEL_KEY)
const level =
existingLevel ||
(await upsertWorkerLevel({
levelKey: DEFAULT_LEVEL_KEY,
name: '普通打手',
sortOrder: 100,
name: DEFAULT_LEVEL_NAME,
sortOrder: 10,
status: 'active',
permissionJson: JSON.stringify({
depositFreeAmount: 0,
maxActiveOrders: 3,
maxActiveOrders: 1,
upgradeThreshold: 0,
}),
now,
})
}))
const category = await upsertWorkCategory({
categoryKey: DEFAULT_CATEGORY_KEY,
name: '默认分类',
@@ -113,6 +122,7 @@ export async function registerWorker(payload: JsonObject = {}) {
const password = normalizePassword(payload.password)
const displayName = String(payload.displayName || payload.nickname || username).trim()
const phone = String(payload.phone || '').trim()
const inviteCode = String(payload.inviteCode || '').trim().toUpperCase()
validateWorkerUsername(username)
validateWorkerPassword(password)
@@ -124,6 +134,20 @@ export async function registerWorker(payload: JsonObject = {}) {
})
}
const inviter = inviteCode ? await getWorkerUserByInviteCode(inviteCode) : null
if (inviteCode && !inviter) {
throw createHttpError('邀请码无效', {
statusCode: 400,
errorCode: 'worker_invite_code_invalid',
})
}
if (inviter && inviter.status !== 'active') {
throw createHttpError('邀请人账号不可用', {
statusCode: 400,
errorCode: 'worker_inviter_invalid',
})
}
const defaults = await ensureWorkerPlatformDefaults()
const now = nowIso()
const created = await createWorkerUser({
@@ -133,6 +157,8 @@ export async function registerWorker(payload: JsonObject = {}) {
phone,
levelId: defaults.level?.id || null,
status: 'pending_review',
inviterId: inviter ? Number(inviter.id) : null,
inviteCode: await generateInviteCode(now),
now,
})
@@ -149,6 +175,23 @@ export async function registerWorker(payload: JsonObject = {}) {
}
}
async function generateInviteCode(now: string): Promise<string> {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
for (let attempt = 0; attempt < INVITE_CODE_RETRY_LIMIT; attempt++) {
const candidate = Array.from(
{ length: INVITE_CODE_LENGTH },
() => chars[Math.floor(crypto.randomInt(chars.length))],
).join('')
if (!(await getWorkerUserByInviteCode(candidate))) {
return candidate
}
}
throw createHttpError('邀请码生成失败,请重试', {
statusCode: 500,
errorCode: 'worker_invite_code_generation_failed',
})
}
export async function loginWorker(username: unknown, password: unknown) {
ensureWorkerAuthConfigured()
const normalizedUsername = normalizeUsername(username)
@@ -298,19 +341,61 @@ export async function getWorkerProfile(session: WorkerSession) {
countWorkerAcceptedOrders(session.workerId),
Promise.resolve(getWorkerFinanceConfig()),
])
const permissions = resolveWorkerPermissions(worker)
const levelProgress = await resolveLevelProgress(worker, acceptedOrderCount)
return {
worker: mapWorkerUser(worker),
permissions: resolveWorkerPermissions(worker),
permissions,
summary: {
acceptedOrderCount,
pendingWithdrawAmount: financeSummary.pendingWithdrawAmount,
approvedWithdrawAmount: financeSummary.approvedWithdrawAmount,
pendingRechargeAmount: financeSummary.pendingRechargeAmount,
},
levelProgress,
financeConfig,
}
}
async function resolveLevelProgress(
worker: WorkerUserRow,
acceptedOrderCount: number,
): Promise<{
currentThreshold: number
nextThreshold: number | null
progressPercent: number
} | null> {
if (!worker.level_id) return null
const levels = await listWorkerLevels()
const active = levels.filter((level) => level.status === 'active')
const current = active.find((level) => Number(level.id) === Number(worker.level_id))
if (!current) return null
const currentThreshold = normalizeInteger(safeParseJson(current.permission_json).upgradeThreshold, 0)
const next = active
.filter((level) => normalizeInteger(safeParseJson(level.permission_json).upgradeThreshold, 0) > currentThreshold)
.sort(
(a, b) =>
normalizeInteger(safeParseJson(a.permission_json).upgradeThreshold, 0) -
normalizeInteger(safeParseJson(b.permission_json).upgradeThreshold, 0),
)[0]
const nextThreshold = next
? normalizeInteger(safeParseJson(next.permission_json).upgradeThreshold, 0)
: null
return {
currentThreshold,
nextThreshold,
progressPercent:
nextThreshold && nextThreshold > currentThreshold
? Math.min(
100,
Math.round(
((acceptedOrderCount - currentThreshold) / (nextThreshold - currentThreshold)) * 100,
),
)
: 100,
}
}
export async function listWorkerProfileWalletLedgers(
query: JsonObject = {},
session: WorkerSession,
@@ -798,13 +883,14 @@ export async function saveAdminWorkCategory(payload: JsonObject = {}) {
export async function saveAdminWorkerLevel(payload: JsonObject = {}) {
const now = nowIso()
const levelKey = String(payload.levelKey || payload.level_key || '').trim() || DEFAULT_LEVEL_KEY
const name = String(payload.name || '').trim() || '普通打手'
const name = String(payload.name || '').trim() || DEFAULT_LEVEL_NAME
const permissions = {
depositFreeAmount: normalizeAmountFen(
payload.depositFreeAmount ?? payload.depositFreeAmountYuan,
0,
),
maxActiveOrders: normalizePositiveInteger(payload.maxActiveOrders, 3),
maxActiveOrders: normalizePositiveInteger(payload.maxActiveOrders, 1),
upgradeThreshold: normalizeInteger(payload.upgradeThreshold, 0),
}
const level = await upsertWorkerLevel({
levelKey,
@@ -1564,7 +1650,8 @@ function resolveWorkerPermissions(worker: WorkerUserRow) {
const permission = safeParseJson(worker.level_permission_json)
return {
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 3),
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
}
}
@@ -1588,7 +1675,8 @@ function mapWorkerLevel(level: WorkerLevelRow | null | undefined) {
sortOrder: Number(level.sort_order || 100),
status: level.status,
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 3),
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
createdAt: level.created_at,
updatedAt: level.updated_at,
}
@@ -1645,6 +1733,14 @@ function mapWorkerUser(worker: WorkerUserRow) {
phone: worker.phone,
status: worker.status,
reviewNote: worker.review_note || '',
inviteCode: worker.invite_code || '',
inviter: worker.inviter_id
? {
workerId: Number(worker.inviter_id),
username: worker.inviter_username || '',
displayName: worker.inviter_display_name || worker.inviter_username || '',
}
: null,
level: worker.level_id
? {
levelId: Number(worker.level_id),
@@ -1790,6 +1790,7 @@ function LevelsPanel() {
name: string
depositFreeAmount?: number
maxActiveOrders?: number
upgradeThreshold?: number
}) {
try {
await saveAdminWorkerLevel(values)
@@ -1810,6 +1811,7 @@ function LevelsPanel() {
render: (_, row) => formatMoney(row.depositFreeAmount),
},
{ title: '最大同时接单', dataIndex: 'maxActiveOrders' },
{ title: '升级所需累计完成', dataIndex: 'upgradeThreshold' },
{ title: '状态', dataIndex: 'status' },
]
@@ -1820,10 +1822,11 @@ function LevelsPanel() {
form={form}
layout="inline"
initialValues={{
levelKey: 'regular',
name: '普通打手',
levelKey: 'vip1',
name: 'VIP1',
depositFreeAmount: 0,
maxActiveOrders: 3,
maxActiveOrders: 1,
upgradeThreshold: 0,
}}
onFinish={saveLevel}
>
@@ -1847,6 +1850,13 @@ function LevelsPanel() {
<Form.Item label="最大同时接单" name="maxActiveOrders">
<InputNumber min={1} max={999} />
</Form.Item>
<Form.Item
label="升级所需累计完成单数"
name="upgradeThreshold"
tooltip="打手累计验收通过该单数后自动升级到本等级"
>
<InputNumber min={0} max={100000} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
@@ -34,6 +34,7 @@ export default function WorkerLoginPage() {
password: string
displayName?: string
phone?: string
inviteCode?: string
}) {
setLoading(true)
try {
@@ -98,6 +99,9 @@ export default function WorkerLoginPage() {
<Form.Item label="手机号" name="phone">
<Input />
</Form.Item>
<Form.Item label="邀请码(选填)" name="inviteCode">
<Input maxLength={8} placeholder="填写邀请人邀请码" />
</Form.Item>
<Form.Item
label="密码"
name="password"
@@ -1,5 +1,6 @@
import {
BankOutlined,
CopyOutlined,
CustomerServiceOutlined,
HistoryOutlined,
LockOutlined,
@@ -23,6 +24,7 @@ import {
Input,
InputNumber,
Modal,
Progress,
Row,
Select,
Space,
@@ -114,9 +116,20 @@ export default function WorkerProfilePage() {
const worker = profileQuery.data?.data.worker
const summary = profileQuery.data?.data.summary
const levelProgress = profileQuery.data?.data.levelProgress
const financeConfig = profileQuery.data?.data.financeConfig
const canUseFinanceActions = worker?.status === 'active'
async function copyInviteCode() {
if (!worker?.inviteCode) return
try {
await navigator.clipboard.writeText(worker.inviteCode)
message.success('邀请码已复制')
} catch {
message.error('复制失败,请手动复制')
}
}
const ledgersQuery = useQuery({
queryKey: ['worker-wallet-ledgers', ledgerType, ledgerPage, ledgerPageSize],
queryFn: () =>
@@ -453,6 +466,40 @@ export default function WorkerProfilePage() {
<Tag>{worker.level?.name || '未分级'}</Tag>
</Space>
</Space>
<Space direction="vertical" size={4} style={{ marginTop: 12, width: '100%' }}>
<Space wrap size={[8, 8]}>
<Typography.Text type="secondary">
</Typography.Text>
<Typography.Text strong>{worker.inviteCode || '-'}</Typography.Text>
<Button
size="small"
type="link"
icon={<CopyOutlined />}
disabled={!worker.inviteCode}
onClick={copyInviteCode}
>
</Button>
</Space>
{levelProgress && levelProgress.nextThreshold !== null ? (
<Space direction="vertical" size={0} style={{ width: '100%' }}>
<Typography.Text type="secondary">
{summary?.acceptedOrderCount || 0}
{Math.max(0, levelProgress.nextThreshold - (summary?.acceptedOrderCount || 0))}
</Typography.Text>
<Progress
percent={levelProgress.progressPercent}
size="small"
format={(percent) => `${percent}%`}
/>
</Space>
) : (
<Typography.Text type="secondary">
{summary?.acceptedOrderCount || 0}
</Typography.Text>
)}
</Space>
</Card>
<Row gutter={[16, 16]}>
@@ -542,6 +589,25 @@ export default function WorkerProfilePage() {
<Descriptions.Item label="当前等级">
{worker.level?.name || '-'}
</Descriptions.Item>
<Descriptions.Item label="我的邀请码">
<Space size={4}>
{worker.inviteCode || '-'}
{worker.inviteCode ? (
<Button
size="small"
type="link"
icon={<CopyOutlined />}
onClick={copyInviteCode}
/>
) : null}
</Space>
</Descriptions.Item>
<Descriptions.Item label="邀请人">
{worker.inviter ? worker.inviter.displayName || worker.inviter.username : '-'}
</Descriptions.Item>
<Descriptions.Item label="已累计完成">
{summary?.acceptedOrderCount || 0}
</Descriptions.Item>
<Descriptions.Item label="免押额度">
{formatMoney(worker.level?.permissions.depositFreeAmount || 0)}
</Descriptions.Item>
@@ -48,6 +48,7 @@ export function saveAdminWorkerLevel(payload: {
name: string
depositFreeAmount?: number
maxActiveOrders?: number
upgradeThreshold?: number
}) {
return apiPost<{ level: WorkerLevel }>(
'/api/v1/admin/worker-platform/levels',
+1
View File
@@ -16,6 +16,7 @@ export function registerWorker(payload: {
password: string
displayName?: string
phone?: string
inviteCode?: string
}) {
return apiPost<{ worker: WorkerUser; reviewRequired: boolean }>(
'/api/v1/worker/auth/register',
@@ -5,6 +5,12 @@ export type WorkerUser = {
phone: string
status: string
reviewNote: string
inviteCode: string
inviter: null | {
workerId: number
username: string
displayName: string
}
level: null | {
levelId: number
levelKey: string
@@ -12,6 +18,7 @@ export type WorkerUser = {
permissions: {
depositFreeAmount: number
maxActiveOrders: number
upgradeThreshold: number
}
}
wallet: {
@@ -32,6 +39,12 @@ export type WorkerProfileSummary = {
pendingRechargeAmount: number
}
export type WorkerLevelProgress = {
currentThreshold: number
nextThreshold: number | null
progressPercent: number
}
export type WorkerWalletLedger = {
ledgerId: number
ledgerType: string
@@ -86,6 +99,7 @@ export type WorkerProfileResponse = {
worker: WorkerUser
permissions: Record<string, unknown>
summary: WorkerProfileSummary
levelProgress: WorkerLevelProgress | null
financeConfig: WorkerFinanceConfig
}
@@ -97,6 +111,7 @@ export type WorkerLevel = {
status: string
depositFreeAmount: number
maxActiveOrders: number
upgradeThreshold: number
createdAt: string
updatedAt: string
}