接单平台新增 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
@@ -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
}