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