diff --git a/backend/src/routes/portal.routes.ts b/backend/src/routes/portal.routes.ts index 3f2e388..c2296b6 100644 --- a/backend/src/routes/portal.routes.ts +++ b/backend/src/routes/portal.routes.ts @@ -36,18 +36,30 @@ function portalAuth(req: Request, res: Response, next: NextFunction) { router.post('/login', async (req, res, next) => { try { const data = portalLoginSchema.parse(req.body) - const employee = await prisma.employee.findFirst({ + // 查找所有匹配手机号的在职员工(可能跨组织) + const employees = await prisma.employee.findMany({ where: { phone: data.phone, status: 'ACTIVE' }, + select: { id: true, name: true, department: true, orgId: true, passwordHash: true }, }) - if (!employee || !employee.passwordHash) { + if (employees.length === 0) { return res.status(400).json({ success: false, error: { code: 'AUTH_FAILED', message: '手机号或密码错误' } }) } - const valid = await bcrypt.compare(data.password, employee.passwordHash) - if (!valid) { + // 逐个校验密码,找到匹配的员工 + let matchedEmployee = null + for (const emp of employees) { + if (emp.passwordHash) { + const valid = await bcrypt.compare(data.password, emp.passwordHash) + if (valid) { + matchedEmployee = emp + break + } + } + } + if (!matchedEmployee) { return res.status(400).json({ success: false, error: { code: 'AUTH_FAILED', message: '手机号或密码错误' } }) } - const token = signAccessToken({ id: employee.id, orgId: employee.orgId, role: 'EMPLOYEE' }) - res.json({ success: true, data: { token, employee: { id: employee.id, name: employee.name, department: employee.department } } }) + const token = signAccessToken({ id: matchedEmployee.id, orgId: matchedEmployee.orgId, role: 'EMPLOYEE' }) + res.json({ success: true, data: { token, employee: { id: matchedEmployee.id, name: matchedEmployee.name, department: matchedEmployee.department } } }) } catch (err) { next(err) } @@ -94,10 +106,16 @@ router.post('/verify-code', async (req, res, next) => { return res.status(400).json({ success: false, error: { code: 'CODE_WRONG', message: `验证码错误(剩余${5 - stored.failCount - 1}次机会)` } }) } await deleteCode(data.phone) - const employee = await prisma.employee.findFirst({ where: { phone: data.phone, status: 'ACTIVE' } }) - if (!employee) { + // 查找所有匹配手机号的在职员工(可能跨组织) + const employees = await prisma.employee.findMany({ + where: { phone: data.phone, status: 'ACTIVE' }, + select: { id: true, name: true, department: true, orgId: true }, + }) + if (employees.length === 0) { return res.status(400).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } }) } + // 如果只有一个匹配,直接登录 + const employee = employees[0] const token = signAccessToken({ id: employee.id, orgId: employee.orgId, role: 'EMPLOYEE' }) res.json({ success: true, data: { token, employee: { id: employee.id, name: employee.name, department: employee.department } } }) } catch (err) { diff --git a/backend/src/services/contract.service.ts b/backend/src/services/contract.service.ts index 641aa53..68362a5 100644 --- a/backend/src/services/contract.service.ts +++ b/backend/src/services/contract.service.ts @@ -219,6 +219,16 @@ export async function createEmployee(orgId: string, userId: string, data: any) { throw { code: 'DUPLICATE_ID_CARD', message: `证件号码已存在:${existing.name}(${existing.department},${existing.status === 'ACTIVE' ? '在职' : '离职'}),请确认是否重复录入` } } } + // 手机号查重(同组织内不允许重复,影响员工端登录) + if (data.phone) { + const phoneExists = await prisma.employee.findFirst({ + where: { orgId, phone: data.phone }, + select: { id: true, name: true, department: true, status: true }, + }) + if (phoneExists) { + throw { code: 'DUPLICATE_PHONE', message: `手机号已存在:${phoneExists.name}(${phoneExists.department},${phoneExists.status === 'ACTIVE' ? '在职' : '离职'}),员工端登录需手机号唯一,请确认是否重复录入` } + } + } const org = await prisma.organization.findUnique({ where: { id: orgId } }) if (org && org.maxEmployees > 0) { diff --git a/frontend/src/pages/Contracts.tsx b/frontend/src/pages/Contracts.tsx index 832a86d..7c8af2c 100644 --- a/frontend/src/pages/Contracts.tsx +++ b/frontend/src/pages/Contracts.tsx @@ -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 }: {
- setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} /> + { + 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} />
+ {phoneDuplicate?.exists && ( +
+ + 该手机号已存在:{phoneDuplicate.employee?.name}({phoneDuplicate.employee?.department}),请确认是否重复录入 +
+ )} {/* 特殊状态 */}