登录界面增加忘记密码. 后台增加密码重置
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
|||||||
reviewAdminWorkerFinanceRequest,
|
reviewAdminWorkerFinanceRequest,
|
||||||
resolveAdminProblemWorkOrder,
|
resolveAdminProblemWorkOrder,
|
||||||
reviewAdminWorkerUser,
|
reviewAdminWorkerUser,
|
||||||
|
resetAdminWorkerPassword,
|
||||||
saveAdminWorkerFinanceConfig,
|
saveAdminWorkerFinanceConfig,
|
||||||
saveAdminWorkerProductMatchConfig,
|
saveAdminWorkerProductMatchConfig,
|
||||||
saveAdminWorkerPlatformNotificationConfig,
|
saveAdminWorkerPlatformNotificationConfig,
|
||||||
@@ -318,6 +319,25 @@ router.post(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/worker-platform/workers/:workerId/reset-password',
|
||||||
|
requireAdminRoles(['admin', 'operator']),
|
||||||
|
createJsonHandler(
|
||||||
|
(req) => resetAdminWorkerPassword(String(req.params.workerId || ''), req.body || {}),
|
||||||
|
{
|
||||||
|
successMessage: '打手密码已重置',
|
||||||
|
errorMessage: '重置打手密码失败',
|
||||||
|
scope: '[admin/worker-platform/workers/:workerId/reset-password]',
|
||||||
|
audit: (req, data) => ({
|
||||||
|
action: 'worker_password_reset',
|
||||||
|
targetType: 'worker',
|
||||||
|
targetId: String(req.params.workerId || ''),
|
||||||
|
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/workers/:workerId/wallet-credit',
|
'/worker-platform/workers/:workerId/wallet-credit',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminRoles(['admin', 'operator']),
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import {
|
|||||||
logoutWorkerSession,
|
logoutWorkerSession,
|
||||||
registerWorker,
|
registerWorker,
|
||||||
sendWorkerSmsCode,
|
sendWorkerSmsCode,
|
||||||
|
sendWorkerPasswordResetSmsCode,
|
||||||
|
resetWorkerPassword,
|
||||||
saveWorkerOrderAcceptanceDraft,
|
saveWorkerOrderAcceptanceDraft,
|
||||||
saveWorkerOrderNote,
|
saveWorkerOrderNote,
|
||||||
remindWorkerOrderAcceptance,
|
remindWorkerOrderAcceptance,
|
||||||
@@ -76,6 +78,36 @@ router.post(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/password-reset/sms-code',
|
||||||
|
createRateLimitMiddleware({
|
||||||
|
scope: 'worker:password-reset-sms-code',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 5,
|
||||||
|
key: getBodyFieldRateLimitKey('phone'),
|
||||||
|
}),
|
||||||
|
createRouteHandler((req) => sendWorkerPasswordResetSmsCode(req.body || {}), {
|
||||||
|
successMessage: '验证码已发送',
|
||||||
|
errorMessage: '验证码发送失败',
|
||||||
|
scope: '[worker/auth/password-reset/sms-code]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/password-reset',
|
||||||
|
createRateLimitMiddleware({
|
||||||
|
scope: 'worker:password-reset',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 10,
|
||||||
|
key: getBodyFieldRateLimitKey('phone'),
|
||||||
|
}),
|
||||||
|
createRouteHandler((req) => resetWorkerPassword(req.body || {}), {
|
||||||
|
successMessage: '密码已重置,请重新登录',
|
||||||
|
errorMessage: '密码重置失败',
|
||||||
|
scope: '[worker/auth/password-reset]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/auth/login',
|
'/auth/login',
|
||||||
createRateLimitMiddleware({
|
createRateLimitMiddleware({
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
updateWorkOrder,
|
updateWorkOrder,
|
||||||
updateWorkOrderBasic,
|
updateWorkOrderBasic,
|
||||||
updateWorkerUser,
|
updateWorkerUser,
|
||||||
|
updateWorkerPassword,
|
||||||
revokeAllWorkerSessions,
|
revokeAllWorkerSessions,
|
||||||
upsertWorkerWithdrawalAccount,
|
upsertWorkerWithdrawalAccount,
|
||||||
upsertWorkCategory,
|
upsertWorkCategory,
|
||||||
@@ -123,6 +124,8 @@ import {
|
|||||||
normalizeRequirementFieldsFromPayload,
|
normalizeRequirementFieldsFromPayload,
|
||||||
normalizeReviewStatus,
|
normalizeReviewStatus,
|
||||||
normalizeSessionVersion,
|
normalizeSessionVersion,
|
||||||
|
hashWorkerPassword,
|
||||||
|
validateWorkerPassword,
|
||||||
normalizeSlugKey,
|
normalizeSlugKey,
|
||||||
normalizeStatuses,
|
normalizeStatuses,
|
||||||
normalizeSubmittedFields,
|
normalizeSubmittedFields,
|
||||||
@@ -941,6 +944,30 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload:
|
|||||||
return { worker: mapWorkerUser(updated || worker) }
|
return { worker: mapWorkerUser(updated || worker) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resetAdminWorkerPassword(
|
||||||
|
workerId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
) {
|
||||||
|
const worker = await getRequiredWorker(workerId)
|
||||||
|
const password = String(payload.password || payload.newPassword || '').trim()
|
||||||
|
if (!password) {
|
||||||
|
throw createHttpError('请输入新密码', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_password_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
validateWorkerPassword(password)
|
||||||
|
const now = nowIso()
|
||||||
|
const updated = await updateWorkerPassword({
|
||||||
|
workerId: worker.id,
|
||||||
|
passwordHash: hashWorkerPassword(password),
|
||||||
|
sessionVersion: normalizeSessionVersion(worker.session_version) + 1,
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
await revokeAllWorkerSessions(worker.id, now)
|
||||||
|
return { worker: mapWorkerUser(updated || worker), reloginRequired: true }
|
||||||
|
}
|
||||||
|
|
||||||
export async function assignAdminWorkOrderToWorker(
|
export async function assignAdminWorkOrderToWorker(
|
||||||
workOrderId: number | string,
|
workOrderId: number | string,
|
||||||
payload: JsonObject = {},
|
payload: JsonObject = {},
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
countWorkerWithdrawRequestsOnDay,
|
countWorkerWithdrawRequestsOnDay,
|
||||||
countWorkOrderPendingSharingSubmissions,
|
countWorkOrderPendingSharingSubmissions,
|
||||||
countWorkerSmsCodesOnDay,
|
countWorkerSmsCodesOnDay,
|
||||||
|
consumeWorkerSmsCode,
|
||||||
createWorkerSmsCode,
|
createWorkerSmsCode,
|
||||||
createWorkOrderEvent,
|
createWorkOrderEvent,
|
||||||
createWorkerFinanceRequest,
|
createWorkerFinanceRequest,
|
||||||
@@ -236,6 +237,119 @@ export async function sendWorkerSmsCode(payload: JsonObject = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendWorkerPasswordResetSmsCode(payload: JsonObject = {}) {
|
||||||
|
const phone = normalizeWorkerPhone(payload.phone)
|
||||||
|
const config = runtimeConfig.worker.sms
|
||||||
|
const cooldownMs = Math.max(10, Number(config.cooldownSeconds || 60)) * 1000
|
||||||
|
const dailyLimit = Math.max(1, Number(config.dailyLimit || 10))
|
||||||
|
const ttlSeconds = Math.max(60, Number(config.codeTtlSeconds || 300))
|
||||||
|
|
||||||
|
if (!(await getWorkerUserByPhone(phone))) {
|
||||||
|
throw createHttpError('该手机号未注册', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'worker_phone_not_registered',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const latest = await getLatestWorkerSmsCode(phone, 'password_reset')
|
||||||
|
if (
|
||||||
|
latest &&
|
||||||
|
!latest.consumed_at &&
|
||||||
|
new Date(latest.created_at).getTime() > Date.now() - cooldownMs
|
||||||
|
) {
|
||||||
|
throw createHttpError('验证码发送过于频繁,请稍后再试', {
|
||||||
|
statusCode: 429,
|
||||||
|
errorCode: 'worker_sms_cooldown',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayRange = resolveChinaDayRange()
|
||||||
|
if ((await countWorkerSmsCodesOnDay(phone, dayRange.start, dayRange.end)) >= dailyLimit) {
|
||||||
|
throw createHttpError('今日验证码发送次数已达上限,请明天再试', {
|
||||||
|
statusCode: 429,
|
||||||
|
errorCode: 'worker_sms_daily_limit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = generateSmsCode()
|
||||||
|
await getSmsProvider().sendCode(phone, code)
|
||||||
|
await createWorkerSmsCode({
|
||||||
|
phone,
|
||||||
|
purpose: 'password_reset',
|
||||||
|
codeHash: hashSmsCode(code),
|
||||||
|
expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
|
||||||
|
now: nowIso(),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
sent: true,
|
||||||
|
expiresInSeconds: ttlSeconds,
|
||||||
|
provider: String(runtimeConfig.worker.sms.provider || 'mock'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetWorkerPassword(payload: JsonObject = {}) {
|
||||||
|
const phone = normalizeWorkerPhone(payload.phone)
|
||||||
|
const code = String(payload.code || payload.smsCode || '').trim()
|
||||||
|
const password = normalizePassword(payload.password || payload.newPassword)
|
||||||
|
validateWorkerPassword(password)
|
||||||
|
if (!code) {
|
||||||
|
throw createHttpError('请输入短信验证码', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_sms_code_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = await getWorkerUserByPhone(phone)
|
||||||
|
if (!worker) {
|
||||||
|
throw createHttpError('该手机号未注册', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'worker_phone_not_registered',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const latest = await getLatestWorkerSmsCode(phone, 'password_reset')
|
||||||
|
const codeValid = Boolean(
|
||||||
|
latest &&
|
||||||
|
!latest.consumed_at &&
|
||||||
|
new Date(latest.expires_at).getTime() > Date.now() &&
|
||||||
|
latest.code_hash === hashSmsCode(code),
|
||||||
|
)
|
||||||
|
if (!codeValid) {
|
||||||
|
if (latest && !latest.consumed_at) {
|
||||||
|
const failed = await recordWorkerSmsCodeFailure(
|
||||||
|
latest.id,
|
||||||
|
nowIso(),
|
||||||
|
WORKER_SMS_MAX_VERIFY_ATTEMPTS,
|
||||||
|
)
|
||||||
|
if (failed?.consumed_at) {
|
||||||
|
throw createHttpError('验证码错误次数过多,请重新获取验证码', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_sms_code_locked',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw createHttpError('短信验证码错误或已过期', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_sms_code_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const consumed = await consumeWorkerSmsCode(latest!.id, nowIso())
|
||||||
|
if (!consumed) {
|
||||||
|
throw createHttpError('验证码已失效,请重新获取', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_sms_code_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const now = nowIso()
|
||||||
|
const updated = await updateWorkerPassword({
|
||||||
|
workerId: worker.id,
|
||||||
|
passwordHash: hashWorkerPassword(password),
|
||||||
|
sessionVersion: normalizeSessionVersion(worker.session_version) + 1,
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
await revokeAllWorkerSessions(worker.id, now)
|
||||||
|
return { worker: mapWorkerUser(updated || worker), reloginRequired: true }
|
||||||
|
}
|
||||||
|
|
||||||
export async function registerWorker(payload: JsonObject = {}) {
|
export async function registerWorker(payload: JsonObject = {}) {
|
||||||
ensureWorkerAuthConfigured()
|
ensureWorkerAuthConfigured()
|
||||||
const phone = normalizeWorkerPhone(payload.phone)
|
const phone = normalizeWorkerPhone(payload.phone)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
fetchAdminWorkerLevels,
|
fetchAdminWorkerLevels,
|
||||||
fetchAdminWorkerUsers,
|
fetchAdminWorkerUsers,
|
||||||
fetchAdminWorkerWithdrawalAccounts,
|
fetchAdminWorkerWithdrawalAccounts,
|
||||||
|
resetAdminWorkerPassword,
|
||||||
reviewAdminWorkerUser,
|
reviewAdminWorkerUser,
|
||||||
saveAdminWorkerWithdrawalAccount,
|
saveAdminWorkerWithdrawalAccount,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
@@ -53,12 +54,14 @@ export default function WorkersPanel() {
|
|||||||
const [creditWorker, setCreditWorker] = useState<WorkerUser | null>(null)
|
const [creditWorker, setCreditWorker] = useState<WorkerUser | null>(null)
|
||||||
const [levelWorker, setLevelWorker] = useState<WorkerUser | null>(null)
|
const [levelWorker, setLevelWorker] = useState<WorkerUser | null>(null)
|
||||||
const [withdrawalAccountWorker, setWithdrawalAccountWorker] = useState<WorkerUser | null>(null)
|
const [withdrawalAccountWorker, setWithdrawalAccountWorker] = useState<WorkerUser | null>(null)
|
||||||
|
const [resetPasswordWorker, setResetPasswordWorker] = useState<WorkerUser | null>(null)
|
||||||
const [withdrawalAccounts, setWithdrawalAccounts] = useState<AdminWorkerWithdrawalAccount[]>([])
|
const [withdrawalAccounts, setWithdrawalAccounts] = useState<AdminWorkerWithdrawalAccount[]>([])
|
||||||
const [loadingWithdrawalAccounts, setLoadingWithdrawalAccounts] = useState(false)
|
const [loadingWithdrawalAccounts, setLoadingWithdrawalAccounts] = useState(false)
|
||||||
const [savingWithdrawalAccount, setSavingWithdrawalAccount] = useState(false)
|
const [savingWithdrawalAccount, setSavingWithdrawalAccount] = useState(false)
|
||||||
const [creditForm] = Form.useForm()
|
const [creditForm] = Form.useForm()
|
||||||
const [levelForm] = Form.useForm<{ levelId?: number }>()
|
const [levelForm] = Form.useForm<{ levelId?: number }>()
|
||||||
const [withdrawalAccountForm] = Form.useForm<WithdrawalAccountFormValues>()
|
const [withdrawalAccountForm] = Form.useForm<WithdrawalAccountFormValues>()
|
||||||
|
const [resetPasswordForm] = Form.useForm<{ password: string }>()
|
||||||
const withdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm)
|
const withdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm)
|
||||||
|
|
||||||
const workersQuery = useQuery({
|
const workersQuery = useQuery({
|
||||||
@@ -145,6 +148,21 @@ export default function WorkersPanel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitResetPassword(values: { password: string }) {
|
||||||
|
if (!resetPasswordWorker) return
|
||||||
|
try {
|
||||||
|
await resetAdminWorkerPassword(resetPasswordWorker.workerId, {
|
||||||
|
password: values.password.trim(),
|
||||||
|
})
|
||||||
|
message.success('打手密码已重置,旧登录设备已退出')
|
||||||
|
setResetPasswordWorker(null)
|
||||||
|
resetPasswordForm.resetFields()
|
||||||
|
await refreshWorkers()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '重置打手密码失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitLevel(values: { levelId?: number }) {
|
async function submitLevel(values: { levelId?: number }) {
|
||||||
if (!levelWorker) return
|
if (!levelWorker) return
|
||||||
try {
|
try {
|
||||||
@@ -306,6 +324,7 @@ export default function WorkersPanel() {
|
|||||||
调等级
|
调等级
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setCreditWorker(row)}>充值</Button>
|
<Button onClick={() => setCreditWorker(row)}>充值</Button>
|
||||||
|
<Button onClick={() => setResetPasswordWorker(row)}>重置密码</Button>
|
||||||
<Button onClick={() => void openWithdrawalAccountModal(row)}>提现信息</Button>
|
<Button onClick={() => void openWithdrawalAccountModal(row)}>提现信息</Button>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@@ -387,6 +406,45 @@ export default function WorkersPanel() {
|
|||||||
scroll={{ x: 1000 }}
|
scroll={{ x: 1000 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="重置打手密码"
|
||||||
|
open={Boolean(resetPasswordWorker)}
|
||||||
|
onCancel={() => {
|
||||||
|
setResetPasswordWorker(null)
|
||||||
|
resetPasswordForm.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => resetPasswordForm.submit()}
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<Form form={resetPasswordForm} layout="vertical" onFinish={submitResetPassword}>
|
||||||
|
<Form.Item label="打手">
|
||||||
|
<Typography.Text>
|
||||||
|
{resetPasswordWorker?.displayName || resetPasswordWorker?.username || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="新密码"
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入新密码' },
|
||||||
|
{ min: 6, message: '密码至少 6 位' },
|
||||||
|
{
|
||||||
|
validator: (_, value) =>
|
||||||
|
!value ||
|
||||||
|
(/[A-Z]/.test(value) &&
|
||||||
|
/[a-z]/.test(value) &&
|
||||||
|
/\d/.test(value) &&
|
||||||
|
/[\p{P}\p{S}]/u.test(value))
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject(new Error('密码需包含大写字母、小写字母、数字和标点符号')),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="至少 6 位,包含大小写、数字和标点" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="编辑打手提现信息"
|
title="编辑打手提现信息"
|
||||||
open={Boolean(withdrawalAccountWorker)}
|
open={Boolean(withdrawalAccountWorker)}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import {
|
|||||||
loginWorker,
|
loginWorker,
|
||||||
manageWorkerSessions,
|
manageWorkerSessions,
|
||||||
registerWorker,
|
registerWorker,
|
||||||
|
resetWorkerPassword,
|
||||||
sendWorkerSmsCode,
|
sendWorkerSmsCode,
|
||||||
|
sendWorkerPasswordResetSmsCode,
|
||||||
} from '@/services/worker'
|
} from '@/services/worker'
|
||||||
import type { WorkerSessionDevice } from '@/types/worker-platform'
|
import type { WorkerSessionDevice } from '@/types/worker-platform'
|
||||||
import { hasWorkerSession, setWorkerSession } from '@/utils/worker-auth'
|
import { hasWorkerSession, setWorkerSession } from '@/utils/worker-auth'
|
||||||
@@ -47,8 +49,11 @@ export default function WorkerLoginPage() {
|
|||||||
const { message, modal } = App.useApp()
|
const { message, modal } = App.useApp()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [registerForm] = Form.useForm()
|
const [registerForm] = Form.useForm()
|
||||||
|
const [resetForm] = Form.useForm<{ phone: string; code: string; password: string }>()
|
||||||
const [deviceForm] = Form.useForm<{ username: string; password: string }>()
|
const [deviceForm] = Form.useForm<{ username: string; password: string }>()
|
||||||
const [countdown, setCountdown] = useState(0)
|
const [countdown, setCountdown] = useState(0)
|
||||||
|
const [resetCountdown, setResetCountdown] = useState(0)
|
||||||
|
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
|
||||||
const [deviceManagerOpen, setDeviceManagerOpen] = useState(false)
|
const [deviceManagerOpen, setDeviceManagerOpen] = useState(false)
|
||||||
const [deviceManagerLoading, setDeviceManagerLoading] = useState(false)
|
const [deviceManagerLoading, setDeviceManagerLoading] = useState(false)
|
||||||
const [managedDevices, setManagedDevices] = useState<WorkerSessionDevice[]>([])
|
const [managedDevices, setManagedDevices] = useState<WorkerSessionDevice[]>([])
|
||||||
@@ -59,6 +64,12 @@ export default function WorkerLoginPage() {
|
|||||||
return () => window.clearInterval(timer)
|
return () => window.clearInterval(timer)
|
||||||
}, [countdown])
|
}, [countdown])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (resetCountdown <= 0) return
|
||||||
|
const timer = window.setInterval(() => setResetCountdown((value) => value - 1), 1000)
|
||||||
|
return () => window.clearInterval(timer)
|
||||||
|
}, [resetCountdown])
|
||||||
|
|
||||||
if (hasWorkerSession()) {
|
if (hasWorkerSession()) {
|
||||||
return <Navigate to="/hall" replace />
|
return <Navigate to="/hall" replace />
|
||||||
}
|
}
|
||||||
@@ -146,6 +157,42 @@ export default function WorkerLoginPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function requestResetSmsCode() {
|
||||||
|
const phone = String(resetForm.getFieldValue('phone') || '').trim()
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||||
|
message.error('请先填写正确的手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await sendWorkerPasswordResetSmsCode({ phone })
|
||||||
|
setResetCountdown(SMS_COUNTDOWN_SECONDS)
|
||||||
|
message.success(
|
||||||
|
response.data.provider === 'mock'
|
||||||
|
? '验证码已发送(开发模式,验证码见服务日志)'
|
||||||
|
: `验证码已发送,${response.data.expiresInSeconds} 秒内有效`,
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '验证码发送失败')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitResetPassword(values: { phone: string; code: string; password: string }) {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
await resetWorkerPassword(values)
|
||||||
|
message.success('密码已重置,请使用新密码登录')
|
||||||
|
setForgotPasswordOpen(false)
|
||||||
|
resetForm.resetFields()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '密码重置失败')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitRegister(values: {
|
async function submitRegister(values: {
|
||||||
phone: string
|
phone: string
|
||||||
code: string
|
code: string
|
||||||
@@ -269,6 +316,9 @@ export default function WorkerLoginPage() {
|
|||||||
>
|
>
|
||||||
登录设备管理
|
登录设备管理
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="link" block onClick={() => setForgotPasswordOpen(true)}>
|
||||||
|
忘记密码?
|
||||||
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -377,6 +427,70 @@ export default function WorkerLoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
<Modal
|
||||||
|
title="忘记密码"
|
||||||
|
open={forgotPasswordOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
setForgotPasswordOpen(false)
|
||||||
|
resetForm.resetFields()
|
||||||
|
}}
|
||||||
|
footer={null}
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={resetForm}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={submitResetPassword}
|
||||||
|
requiredMark={false}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="手机号"
|
||||||
|
name="phone"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入手机号' },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input prefix={<MobileOutlined />} maxLength={11} placeholder="注册时使用的手机号" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="短信验证码"
|
||||||
|
name="code"
|
||||||
|
rules={[{ required: true, message: '请输入短信验证码' }]}
|
||||||
|
>
|
||||||
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
|
<Input
|
||||||
|
prefix={<SafetyCertificateOutlined />}
|
||||||
|
maxLength={6}
|
||||||
|
placeholder="6 位数字验证码"
|
||||||
|
/>
|
||||||
|
<Button loading={loading} disabled={resetCountdown > 0} onClick={requestResetSmsCode}>
|
||||||
|
{resetCountdown > 0 ? `${resetCountdown}s 后重发` : '获取验证码'}
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="新密码"
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入新密码' },
|
||||||
|
{
|
||||||
|
validator: (_, value) =>
|
||||||
|
!value || isWorkerPasswordStrong(value)
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject(new Error(WORKER_PASSWORD_RULE_MESSAGE)),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
extra={WORKER_PASSWORD_RULE_MESSAGE}
|
||||||
|
>
|
||||||
|
<Input.Password prefix={<LockOutlined />} autoComplete="new-password" />
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" loading={loading} size="large" block>
|
||||||
|
重置密码
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="登录设备管理"
|
title="登录设备管理"
|
||||||
open={deviceManagerOpen}
|
open={deviceManagerOpen}
|
||||||
|
|||||||
@@ -227,6 +227,13 @@ export function reviewAdminWorkerUser(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resetAdminWorkerPassword(workerId: number, payload: { password: string }) {
|
||||||
|
return apiPost<{ worker: WorkerUser; reloginRequired: boolean }>(
|
||||||
|
`/api/v1/admin/worker-platform/workers/${workerId}/reset-password`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchAdminWorkerWithdrawalAccounts(workerId: number) {
|
export function fetchAdminWorkerWithdrawalAccounts(workerId: number) {
|
||||||
return apiGet<{ worker: WorkerUser; items: AdminWorkerWithdrawalAccount[] }>(
|
return apiGet<{ worker: WorkerUser; items: AdminWorkerWithdrawalAccount[] }>(
|
||||||
`/api/v1/admin/worker-platform/workers/${workerId}/withdrawal-accounts`,
|
`/api/v1/admin/worker-platform/workers/${workerId}/withdrawal-accounts`,
|
||||||
|
|||||||
@@ -39,6 +39,20 @@ export function registerWorker(payload: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sendWorkerPasswordResetSmsCode(payload: { phone: string }) {
|
||||||
|
return apiPost<{ sent: boolean; expiresInSeconds: number; provider: string }>(
|
||||||
|
'/api/v1/worker/auth/password-reset/sms-code',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetWorkerPassword(payload: { phone: string; code: string; password: string }) {
|
||||||
|
return apiPost<{ worker: WorkerUser; reloginRequired: boolean }>(
|
||||||
|
'/api/v1/worker/auth/password-reset',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function loginWorker(payload: { username: string; password: string }) {
|
export function loginWorker(payload: { username: string; password: string }) {
|
||||||
const userAgent = navigator.userAgent
|
const userAgent = navigator.userAgent
|
||||||
const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(userAgent)
|
const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(userAgent)
|
||||||
|
|||||||
Reference in New Issue
Block a user