优化打手注册和列表筛选

This commit is contained in:
yml2213
2026-08-20 09:52:51 +08:00
parent f1575aec85
commit 12ca943170
14 changed files with 99 additions and 35 deletions
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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',
}) })
+3 -3
View File
@@ -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 || '-'
} }