From 3b4833b51ae41b13b25a5fa0c4c1f03e4becdaca Mon Sep 17 00:00:00 2001 From: jacy-dukang Date: Sun, 12 Jul 2026 17:53:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E5=90=88=E4=BC=99=E4=BA=BA=E5=AD=90?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E8=A6=81=E9=AA=8C=E8=AF=81=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/h5-partner/src/lib/staff.ts | 9 ++ apps/h5-partner/src/pages/StaffCreatePage.tsx | 86 ++++++++++++++++++- packages/shared-types/src/partner-log.ts | 2 + packages/shared-types/src/partner.ts | 1 + .../src/modules/iam/dto/partner-staff.dto.ts | 10 +++ .../modules/iam/partner-staff.controller.ts | 11 ++- .../src/modules/iam/partner-staff.service.ts | 60 ++++++++++++- 7 files changed, 176 insertions(+), 3 deletions(-) diff --git a/apps/h5-partner/src/lib/staff.ts b/apps/h5-partner/src/lib/staff.ts index bb2a5d7..083cb6c 100644 --- a/apps/h5-partner/src/lib/staff.ts +++ b/apps/h5-partner/src/lib/staff.ts @@ -10,12 +10,21 @@ export function listPartnerStaff() { return request('PARTNER_H5', '/partner/staff'); } +export function sendPartnerStaffPhoneSms(phone: string) { + return request<{ ok: boolean; maskedPhone: string }>('PARTNER_H5', '/partner/staff/send-phone-sms', { + method: 'POST', + body: JSON.stringify({ phone: phone.trim() }), + silent: true, + }); +} + export function createPartnerStaff(body: CreatePartnerStaffRequest) { return request('PARTNER_H5', '/partner/staff', { method: 'POST', body: JSON.stringify({ name: body.name, phone: body.phone, + smsCode: body.smsCode, staffRole: PartnerStaffRole.INTERNAL, }), }); diff --git a/apps/h5-partner/src/pages/StaffCreatePage.tsx b/apps/h5-partner/src/pages/StaffCreatePage.tsx index a772a94..bb5ef2c 100644 --- a/apps/h5-partner/src/pages/StaffCreatePage.tsx +++ b/apps/h5-partner/src/pages/StaffCreatePage.tsx @@ -1,18 +1,22 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import PageHeader from '@dukang/shared-ui/PageHeader'; -import { createPartnerStaff } from '../lib/staff'; +import { createPartnerStaff, sendPartnerStaffPhoneSms } from '../lib/staff'; import { toastError, toastSuccess } from '../lib/toast'; type FieldErrors = { name?: string; phone?: string; + smsCode?: string; }; export default function StaffCreatePage() { const navigate = useNavigate(); const [name, setName] = useState(''); const [phone, setPhone] = useState(''); + const [smsCode, setSmsCode] = useState(''); + const [smsCooldown, setSmsCooldown] = useState(0); + const [smsHint, setSmsHint] = useState(''); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(''); const [fieldErrors, setFieldErrors] = useState({}); @@ -25,13 +29,51 @@ export default function StaffCreatePage() { function patchPhone(value: string) { setPhone(value); + setSmsHint(''); setSubmitError(''); + setFieldErrors((prev) => ({ ...prev, phone: undefined, smsCode: undefined })); + if (smsCode) setSmsCode(''); + } + + function patchSmsCode(value: string) { + setSmsCode(value.replace(/\D/g, '')); + setSubmitError(''); + setFieldErrors((prev) => ({ ...prev, smsCode: undefined })); + } + + async function sendPhoneCode() { + const trimmedPhone = phone.trim(); + if (!/^1[3-9]\d{9}$/.test(trimmedPhone)) { + setFieldErrors({ phone: '请先填写正确的11位手机号' }); + return; + } + + setSmsHint(''); setFieldErrors((prev) => ({ ...prev, phone: undefined })); + + try { + const res = await sendPartnerStaffPhoneSms(trimmedPhone); + setSmsHint(`验证码已发送至 ${res.maskedPhone}`); + setSmsCooldown(60); + const timer = setInterval(() => { + setSmsCooldown((s) => { + if (s <= 1) { + clearInterval(timer); + return 0; + } + return s - 1; + }); + }, 1000); + } catch (e) { + const msg = e instanceof Error ? e.message : '验证码发送失败'; + setFieldErrors({ phone: msg }); + } } async function submit() { const trimmedName = name.trim(); const trimmedPhone = phone.trim(); + const trimmedSmsCode = smsCode.trim(); const nextFieldErrors: FieldErrors = {}; if (!trimmedName) nextFieldErrors.name = '请填写真实姓名'; @@ -40,6 +82,11 @@ export default function StaffCreatePage() { } else if (!/^1[3-9]\d{9}$/.test(trimmedPhone)) { nextFieldErrors.phone = '请输入正确的11位手机号'; } + if (!trimmedSmsCode) { + nextFieldErrors.smsCode = '请输入手机号验证码'; + } else if (!/^\d{4,6}$/.test(trimmedSmsCode)) { + nextFieldErrors.smsCode = '验证码格式不正确'; + } if (Object.keys(nextFieldErrors).length) { setFieldErrors(nextFieldErrors); @@ -54,11 +101,16 @@ export default function StaffCreatePage() { await createPartnerStaff({ name: trimmedName, phone: trimmedPhone, + smsCode: trimmedSmsCode, }); toastSuccess('子账号创建成功'); navigate('/center/staff'); } catch (e) { const message = e instanceof Error ? e.message : '创建失败'; + if (message.includes('验证码')) { + setFieldErrors({ smsCode: message }); + return; + } if (message.includes('手机号')) { setFieldErrors({ phone: message }); return; @@ -114,6 +166,38 @@ export default function StaffCreatePage() {

{fieldErrors.phone}

)} + +
+ +
+
+ shield + patchSmsCode(e.target.value)} + /> +
+ +
+ {smsHint && ( +

{smsHint}

+ )} + {fieldErrors.smsCode && ( +

{fieldErrors.smsCode}

+ )} +
diff --git a/packages/shared-types/src/partner-log.ts b/packages/shared-types/src/partner-log.ts index d3e6f6a..2297611 100644 --- a/packages/shared-types/src/partner-log.ts +++ b/packages/shared-types/src/partner-log.ts @@ -12,6 +12,8 @@ export const PARTNER_LOG_EVENT_CATEGORIES: Record this.toStaffItem(row)); } + async sendStaffPhoneSms(actor: AuthUser, phone: string) { + const parentAccountId = actor.actorId; + const parent = await this.prisma.partnerAccount.findUniqueOrThrow({ + where: { id: parentAccountId }, + }); + if (parent.isPrimary !== 1) { + throw new BadRequestException('仅主账号可添加子账号'); + } + + const normalized = phone.trim(); + if (!/^1[3-9]\d{9}$/.test(normalized)) { + throw new BadRequestException('请输入正确的手机号码'); + } + + const existing = await this.prisma.partnerAccount.findUnique({ where: { phone: normalized } }); + if (existing) throw new BadRequestException('该手机号已被使用'); + + const masked = this.maskPhone(normalized); + try { + await this.authService.sendSms(normalized, SmsScene.PARTNER_STAFF_ADD, { + clientApp: ClientApp.PARTNER_H5, + }); + this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_send', parent.id, { + phone: masked, + scene: SmsScene.PARTNER_STAFF_ADD, + status: 'success', + }); + } catch (err) { + if (err instanceof BadRequestException) { + this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_send', parent.id, { + phone: masked, + scene: SmsScene.PARTNER_STAFF_ADD, + status: 'failed', + reason: err.message, + }); + } + throw err; + } + + return { ok: true, maskedPhone: masked }; + } + async createStaff(actor: AuthUser, dto: CreatePartnerStaffDto) { const parentAccountId = actor.actorId; const parent = await this.prisma.partnerAccount.findUniqueOrThrow({ @@ -42,6 +86,18 @@ export class PartnerStaffService { const existing = await this.prisma.partnerAccount.findUnique({ where: { phone } }); if (existing) throw new BadRequestException('该手机号已被使用'); + const smsCode = dto.smsCode.trim(); + if (!smsCode) throw new BadRequestException('请输入手机号验证码'); + try { + await this.authService.verifySmsCode(phone, smsCode, SmsScene.PARTNER_STAFF_ADD); + } catch (err) { + this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_verify_fail', parentAccountId, { + phone: this.maskPhone(phone), + reason: err instanceof BadRequestException ? err.message : '验证码错误', + }); + throw err; + } + const name = dto.name.trim(); if (!name) throw new BadRequestException('请填写真实姓名'); @@ -64,6 +120,7 @@ export class PartnerStaffService { phone: this.maskPhone(phone), staffRole, status: account.status, + phoneVerified: true, }); return this.toStaffItem(account); @@ -180,3 +237,4 @@ export class PartnerStaffService { return `${phone.slice(0, 3)} **** ${phone.slice(7)}`; } } + \ No newline at end of file