fix: 手机号查重硬校验 + portal 登录跨组织安全

问题:
1. 后端 createEmployee 只有身份证查重,没有手机号查重,
   同组织内可重复录入相同手机号,导致员工端登录混乱
2. portal 登录用 findFirst 按 phone 查,未考虑跨组织重复,
   多组织同手机号时会登录到错误员工
3. Contracts.tsx 的 AddEmployeeModal 完全没有手机号查重

修复:
1. contract.service.ts createEmployee 添加手机号查重硬校验
   (同组织内 phone 唯一,抛 DUPLICATE_PHONE 错误)
2. portal.routes.ts 密码登录改为 findMany 遍历校验密码,
   验证码登录改为 findMany 取第一个匹配
3. Contracts.tsx AddEmployeeModal 添加手机号查重和警告提示

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-16 10:57:06 +08:00
parent 13f485a049
commit 6b3e1f9d46
3 changed files with 55 additions and 10 deletions
+19 -2
View File
@@ -1,7 +1,7 @@
import { useState, useRef } from 'react'
import { usePageSize } from '../hooks/usePageSize'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Plus, Search, Paperclip, Trash2, X, FileText, Download } from 'lucide-react'
import { Plus, Search, Paperclip, Trash2, X, FileText, Download, AlertTriangle } from 'lucide-react'
import { toast } from 'sonner'
import { rosterApi, employeeApi, attachmentApi } from '../lib/api-services'
import api from '../lib/api'
@@ -232,6 +232,8 @@ function AddEmployeeModal({ open, onClose, onSubmit, loading, error }: {
})
}
buildDeptOptions(departments, null, 0)
// 手机号查重
const [phoneDuplicate, setPhoneDuplicate] = useState<{ exists: boolean; employee?: any } | null>(null)
const [form, setForm] = useState({
name: '',
department: '',
@@ -330,9 +332,24 @@ function AddEmployeeModal({ open, onClose, onSubmit, loading, error }: {
</div>
<div>
<Label></Label>
<Input value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} />
<Input value={form.phone} onChange={(e) => {
const phone = e.target.value.replace(/\D/g, '').slice(0, 11)
setForm({ ...form, phone })
setPhoneDuplicate(null)
if (phone.length === 11) {
employeeApi.checkPhone(phone).then((data: { exists: boolean; employee?: any }) => {
setPhoneDuplicate(data)
}).catch(() => {})
}
}} placeholder="选填" maxLength={11} />
</div>
</div>
{phoneDuplicate?.exists && (
<div className="px-3 py-2 rounded-md bg-amber-50 text-amber-700 text-xs flex items-center gap-2">
<AlertTriangle className="w-4 h-4 shrink-0" />
<span>{phoneDuplicate.employee?.name}{phoneDuplicate.employee?.department}</span>
</div>
)}
{/* 特殊状态 */}
<div className="flex gap-4">