优化打手注册和列表筛选
This commit is contained in:
@@ -239,7 +239,7 @@ bash deploy/ubuntu-deploy.sh
|
||||
| 等级权限 | VIP 等级体系:免押额度、最大同时接单量、大厅可见延迟 |
|
||||
| 资金 | 押金冻结 / 退还 / 扣除流水,充值申请与提现审核 |
|
||||
| 物品规则 | 按商品/SKU 匹配自动生成工单,可继承默认时限与拼单配置 |
|
||||
| 管理后台 | 接单工单 CRUD、打手审核、问题单处置、定时任务管理,功能标签页记忆上次选择 |
|
||||
| 管理后台 | 接单工单 CRUD、打手账号管理、问题单处置、定时任务管理,功能标签页记忆上次选择 |
|
||||
|
||||
说明:
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 短信验证码注册已完成手机号校验,历史遗留的待审核账号统一自动启用。
|
||||
UPDATE worker_users
|
||||
SET status = 'active',
|
||||
review_note = CASE
|
||||
WHEN BTRIM(review_note) = '' THEN '短信验证码注册自动启用'
|
||||
ELSE review_note
|
||||
END,
|
||||
reviewed_at = COALESCE(reviewed_at, updated_at, NOW()),
|
||||
updated_at = NOW()
|
||||
WHERE status = 'pending_review';
|
||||
|
||||
COMMENT ON TABLE worker_users IS '打手账号,手机号验证码注册后自动启用;后台可冻结或调整状态';
|
||||
@@ -284,6 +284,8 @@ export type ListInput = {
|
||||
/** 后台待办跳转时按工单主键精确定位 */
|
||||
workOrderId?: number
|
||||
keyword?: string
|
||||
/** 打手类型筛选:internal 内部打手 / external 外部打手 */
|
||||
workerType?: string
|
||||
workerId?: number
|
||||
categoryId?: number
|
||||
workerSharingId?: number
|
||||
|
||||
@@ -462,8 +462,9 @@ export async function listWorkerUsers({
|
||||
pageSize = 20,
|
||||
status = '',
|
||||
keyword = '',
|
||||
workerType = '',
|
||||
}: ListInput = {}): Promise<{ items: WorkerUserRow[]; total: number }> {
|
||||
const { whereClause, params } = buildWorkerUserWhere({ status, keyword })
|
||||
const { whereClause, params } = buildWorkerUserWhere({ status, keyword, workerType })
|
||||
const totalResult = await query<{ total: number }>(
|
||||
`SELECT COUNT(*)::int AS total FROM worker_users wu ${whereClause}`,
|
||||
params,
|
||||
@@ -1174,7 +1175,8 @@ async function getWorkerFinanceRequestByIdWithClient(
|
||||
function buildWorkerUserWhere({
|
||||
status = '',
|
||||
keyword = '',
|
||||
}: Pick<ListInput, 'status' | 'keyword'>) {
|
||||
workerType = '',
|
||||
}: Pick<ListInput, 'status' | 'keyword' | 'workerType'>) {
|
||||
const filters: string[] = []
|
||||
const params: unknown[] = []
|
||||
if (status) {
|
||||
@@ -1187,6 +1189,10 @@ function buildWorkerUserWhere({
|
||||
`(wu.username ILIKE $${params.length} OR wu.display_name ILIKE $${params.length} OR wu.phone ILIKE $${params.length})`,
|
||||
)
|
||||
}
|
||||
if (workerType) {
|
||||
params.push(workerType)
|
||||
filters.push(`wu.worker_type = $${params.length}`)
|
||||
}
|
||||
return {
|
||||
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
|
||||
params,
|
||||
|
||||
@@ -305,8 +305,8 @@ router.post(
|
||||
createJsonHandler(
|
||||
(req) => reviewAdminWorkerUser(String(req.params.workerId || ''), req.body || {}),
|
||||
{
|
||||
successMessage: '打手审核状态已更新',
|
||||
errorMessage: '审核打手失败',
|
||||
successMessage: '打手账号状态已更新',
|
||||
errorMessage: '更新打手账号状态失败',
|
||||
scope: '[admin/worker-platform/workers/:workerId/review]',
|
||||
audit: (req, data) => ({
|
||||
action: 'worker_reviewed',
|
||||
|
||||
@@ -70,7 +70,7 @@ router.post(
|
||||
key: getBodyFieldRateLimitKey('phone'),
|
||||
}),
|
||||
createRouteHandler((req) => registerWorker(req.body || {}), {
|
||||
successMessage: '注册成功,请等待后台审核',
|
||||
successMessage: '注册成功,请使用手机号登录',
|
||||
errorMessage: '接单账号注册失败',
|
||||
scope: '[worker/auth/register]',
|
||||
}),
|
||||
|
||||
@@ -893,6 +893,7 @@ export async function listAdminWorkerUsers(query: JsonObject = {}) {
|
||||
pageSize,
|
||||
status: String(query.status || '').trim(),
|
||||
keyword: String(query.keyword || '').trim(),
|
||||
workerType: String(query.workerType || query.worker_type || '').trim(),
|
||||
})
|
||||
const timeoutCounts = await countTimeoutEventsByWorkerIds(items.map((item) => Number(item.id)))
|
||||
return {
|
||||
|
||||
@@ -328,7 +328,8 @@ export async function registerWorker(payload: JsonObject = {}) {
|
||||
displayName,
|
||||
phone,
|
||||
levelId: defaults.level?.id || null,
|
||||
status: 'pending_review',
|
||||
// 手机号验证码已完成注册身份校验,账号直接启用,无需人工审核。
|
||||
status: 'active',
|
||||
inviterId: inviter ? Number(inviter.id) : null,
|
||||
inviteCode: await generateInviteCode(now),
|
||||
smsCodeId: latest!.id,
|
||||
@@ -344,7 +345,8 @@ export async function registerWorker(payload: JsonObject = {}) {
|
||||
|
||||
return {
|
||||
worker: mapWorkerUser(created),
|
||||
reviewRequired: true,
|
||||
// 保留字段兼容旧版客户端,但注册流程不再需要后台审核。
|
||||
reviewRequired: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,7 +512,7 @@ export function assertWorkerLoginAllowed(worker: WorkerUserRow) {
|
||||
}
|
||||
|
||||
if (worker.status === 'pending_review') {
|
||||
throw createHttpError('接单账号待审核,暂无法登录', {
|
||||
throw createHttpError('接单账号待处理,暂无法登录', {
|
||||
statusCode: 403,
|
||||
errorCode: 'worker_pending_review',
|
||||
})
|
||||
|
||||
@@ -352,9 +352,9 @@ function resolveSelectedKey(pathname: string) {
|
||||
}
|
||||
|
||||
function formatWorkerStatus(status: string) {
|
||||
if (status === 'active') return '已审核'
|
||||
if (status === 'pending_review') return '待审核'
|
||||
if (status === 'rejected') return '审核未通过'
|
||||
if (status === 'active') return '已启用'
|
||||
if (status === 'pending_review') return '待处理'
|
||||
if (status === 'rejected') return '已冻结'
|
||||
if (status === 'disabled') return '已停用'
|
||||
return '未登录'
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ function SummaryCards() {
|
||||
const summary = summaryQuery.data?.data
|
||||
|
||||
const items = [
|
||||
{ label: '待审核打手', value: summary?.pendingWorkerCount || 0 },
|
||||
{ label: '待处理打手', value: summary?.pendingWorkerCount || 0 },
|
||||
{ label: '待完善订单', value: summary?.pendingMaterialCount || 0 },
|
||||
{ label: '大厅订单', value: summary?.openOrderCount || 0 },
|
||||
{ label: '代练中', value: summary?.inProgressOrderCount || 0 },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
App,
|
||||
@@ -45,6 +45,9 @@ export default function WorkersPanel() {
|
||||
const { message, modal } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [status, setStatus] = useState('')
|
||||
const [keywordInput, setKeywordInput] = useState('')
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [workerType, setWorkerType] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||
const [creditWorker, setCreditWorker] = useState<WorkerUser | null>(null)
|
||||
@@ -59,8 +62,8 @@ export default function WorkersPanel() {
|
||||
const withdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm)
|
||||
|
||||
const workersQuery = useQuery({
|
||||
queryKey: ['admin-worker-platform-workers', status, page, pageSize],
|
||||
queryFn: () => fetchAdminWorkerUsers({ status, page, pageSize }),
|
||||
queryKey: ['admin-worker-platform-workers', status, keyword, workerType, page, pageSize],
|
||||
queryFn: () => fetchAdminWorkerUsers({ status, keyword, workerType, page, pageSize }),
|
||||
})
|
||||
const levelsQuery = useQuery({
|
||||
queryKey: ['admin-worker-platform-levels'],
|
||||
@@ -79,6 +82,19 @@ export default function WorkersPanel() {
|
||||
])
|
||||
}
|
||||
|
||||
function searchWorkers() {
|
||||
setKeyword(keywordInput.trim())
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
function clearWorkerFilters() {
|
||||
setKeywordInput('')
|
||||
setKeyword('')
|
||||
setStatus('')
|
||||
setWorkerType('')
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
async function review(worker: WorkerUser, nextStatus: string) {
|
||||
try {
|
||||
await reviewAdminWorkerUser(worker.workerId, { status: nextStatus })
|
||||
@@ -269,7 +285,7 @@ export default function WorkersPanel() {
|
||||
render: (_, row) => (
|
||||
<Space wrap>
|
||||
{row.status !== 'active' ? (
|
||||
<Button onClick={() => review(row, 'active')}>通过</Button>
|
||||
<Button onClick={() => review(row, 'active')}>启用</Button>
|
||||
) : null}
|
||||
{row.status !== 'rejected' ? (
|
||||
<Button onClick={() => review(row, 'rejected')}>拒绝</Button>
|
||||
@@ -300,22 +316,49 @@ export default function WorkersPanel() {
|
||||
<Card
|
||||
title="打手"
|
||||
extra={
|
||||
<Space>
|
||||
<Space wrap className="filter-form">
|
||||
<Input
|
||||
allowClear
|
||||
value={keywordInput}
|
||||
placeholder="账号 / 昵称 / 手机号"
|
||||
style={{ width: 220 }}
|
||||
onChange={(event) => setKeywordInput(event.target.value)}
|
||||
onPressEnter={searchWorkers}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
value={status}
|
||||
style={{ width: 150 }}
|
||||
options={[
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'pending_review', label: '待审核' },
|
||||
{ value: 'active', label: '已通过' },
|
||||
{ value: 'rejected', label: '已拒绝' },
|
||||
{ value: 'pending_review', label: '待处理' },
|
||||
{ value: 'active', label: '已启用' },
|
||||
{ value: 'rejected', label: '已冻结' },
|
||||
{ value: 'disabled', label: '已停用' },
|
||||
]}
|
||||
onChange={(nextStatus) => {
|
||||
setStatus(nextStatus)
|
||||
setStatus(nextStatus || '')
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
value={workerType || undefined}
|
||||
style={{ width: 140 }}
|
||||
placeholder="全部类型"
|
||||
options={[
|
||||
{ value: 'external', label: '外部打手' },
|
||||
{ value: 'internal', label: '内部打手' },
|
||||
]}
|
||||
onChange={(nextType) => {
|
||||
setWorkerType(nextType || '')
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
<Button type="primary" icon={<SearchOutlined />} onClick={searchWorkers}>
|
||||
查询
|
||||
</Button>
|
||||
<Button onClick={clearWorkerFilters}>清空筛选</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={workersQuery.isFetching}
|
||||
|
||||
@@ -226,9 +226,9 @@ export function resolveStatusColor(status: string) {
|
||||
|
||||
export function formatWorkerStatus(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
pending_review: '待审核',
|
||||
active: '已通过',
|
||||
rejected: '已拒绝',
|
||||
pending_review: '待处理',
|
||||
active: '已启用',
|
||||
rejected: '已冻结',
|
||||
disabled: '已停用',
|
||||
}
|
||||
return labels[status] || status
|
||||
|
||||
@@ -155,10 +155,8 @@ export default function WorkerLoginPage() {
|
||||
}) {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await registerWorker(values)
|
||||
message.success(
|
||||
response.data.reviewRequired ? '注册成功,请等待后台审核' : '注册成功,请使用手机号登录',
|
||||
)
|
||||
await registerWorker(values)
|
||||
message.success('注册成功,请使用手机号登录')
|
||||
registerForm.resetFields()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '注册失败')
|
||||
|
||||
@@ -944,7 +944,7 @@ export default function WorkerProfilePage() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="worker-profile-mobile-account-item">
|
||||
<span className="worker-profile-mobile-acc-label">审核状态</span>
|
||||
<span className="worker-profile-mobile-acc-label">账号状态</span>
|
||||
<span className="worker-profile-mobile-acc-val">
|
||||
<Tag color={resolveWorkerStatusColor(worker.status)}>
|
||||
{formatWorkerStatus(worker.status)}
|
||||
@@ -1184,7 +1184,7 @@ export default function WorkerProfilePage() {
|
||||
<Descriptions.Item label="手机号">
|
||||
{worker.phone || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="审核状态">
|
||||
<Descriptions.Item label="账号状态">
|
||||
<Tag color={resolveWorkerStatusColor(worker.status)}>
|
||||
{formatWorkerStatus(worker.status)}
|
||||
</Tag>
|
||||
@@ -1814,7 +1814,7 @@ export default function WorkerProfilePage() {
|
||||
<Descriptions.Item label="手机号">
|
||||
<Typography.Text copyable>{worker?.phone || '-'}</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="审核状态">
|
||||
<Descriptions.Item label="账号状态">
|
||||
{formatWorkerStatus(worker?.status || '')}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="当前余额">
|
||||
@@ -1852,9 +1852,9 @@ function formatMoney(value: number | undefined) {
|
||||
}
|
||||
|
||||
function formatWorkerStatus(status: string) {
|
||||
if (status === 'active') return '已审核'
|
||||
if (status === 'pending_review') return '待审核'
|
||||
if (status === 'rejected') return '审核未通过'
|
||||
if (status === 'active') return '已启用'
|
||||
if (status === 'pending_review') return '待处理'
|
||||
if (status === 'rejected') return '已冻结'
|
||||
if (status === 'disabled') return '已停用'
|
||||
return status || '-'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user