feat: 员工端密码登录完整支持

1. createEmployee 创建员工时自动设置默认密码(手机号后6位)
2. 管理员重置密码接口 POST /employees/:id/reset-password
   (重置为手机号后6位)
3. 员工自己修改密码接口 POST /portal/change-password
   (需验证旧密码,新密码至少6位)
4. 花名册详情添加"重置密码"按钮,显示默认密码提示
5. 员工端导航栏添加"修改密码"入口,弹窗修改密码

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 11:28:56 +08:00
parent 26d0b7070d
commit d7fdec49fa
6 changed files with 201 additions and 33 deletions
+25
View File
@@ -1,4 +1,5 @@
import { Router } from 'express'
import bcrypt from 'bcryptjs'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { auditLog } from '../middleware/auditLog'
import { createEvidence } from '../services/evidence.service'
@@ -195,6 +196,30 @@ router.delete('/:id', authMiddleware, async (req: AuthRequest, res, next) => {
}
})
/**
* 管理员重置员工密码(重置为手机号后6位)
* POST /employees/:id/reset-password
*/
router.post('/:id/reset-password', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const employee = await prisma.employee.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
select: { id: true, phone: true, name: true },
})
if (!employee) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } })
}
// 重置为手机号后6位,无手机号则用 123456
const defaultPassword = employee.phone ? employee.phone.slice(-6) : '123456'
const passwordHash = await bcrypt.hash(defaultPassword, 10)
await prisma.employee.update({ where: { id: employee.id }, data: { passwordHash } })
await auditLog(req, 'RESET_PASSWORD', 'EMPLOYEE', employee.id, { employeeName: employee.name })
res.json({ success: true, data: { message: `密码已重置为手机号后6位:${defaultPassword}` } })
} catch (err) {
next(err)
}
})
// 批量续签合规预检
router.post('/contracts/preview-renew', authMiddleware, async (req: AuthRequest, res, next) => {
try {
+32
View File
@@ -123,6 +123,38 @@ router.post('/verify-code', async (req, res, next) => {
}
})
/**
* 员工修改自己的密码
* POST /portal/change-password body: { oldPassword, newPassword }
*/
router.post('/change-password', portalAuth, async (req: any, res, next) => {
try {
const { oldPassword, newPassword } = req.body
if (!oldPassword || !newPassword) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '请输入旧密码和新密码' } })
}
if (newPassword.length < 6) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '新密码至少6位' } })
}
const employee = await prisma.employee.findFirst({
where: { id: req.employee.id },
select: { id: true, passwordHash: true },
})
if (!employee || !employee.passwordHash) {
return res.status(400).json({ success: false, error: { code: 'AUTH_FAILED', message: '当前未设置密码,请联系管理员重置' } })
}
const valid = await bcrypt.compare(oldPassword, employee.passwordHash)
if (!valid) {
return res.status(400).json({ success: false, error: { code: 'AUTH_FAILED', message: '旧密码错误' } })
}
const passwordHash = await bcrypt.hash(newPassword, 10)
await prisma.employee.update({ where: { id: employee.id }, data: { passwordHash } })
res.json({ success: true, data: { message: '密码修改成功' } })
} catch (err) {
next(err)
}
})
// 工资条
router.get('/payslip', portalAuth, async (req: any, res, next) => {
try {
+5
View File
@@ -2,6 +2,7 @@ import prisma from '../lib/prisma'
import { encrypt, decrypt, sha256 } from '../lib/crypto'
import { runRiskDetection } from './risk.service'
import { extractBirthDateFromIdCard, extractGenderFromIdCard } from './retirement.service'
import bcrypt from 'bcryptjs'
function daysBetween(a: Date, b: Date): number {
return Math.floor((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24))
@@ -250,6 +251,9 @@ export async function createEmployee(orgId: string, userId: string, data: any) {
const housingFundStartMonth = data.housingFundStartMonth || hireMonth
const employee = await prisma.$transaction(async (tx) => {
// 默认密码:手机号后6位(员工可在员工端自行修改)
const defaultPassword = data.phone ? data.phone.slice(-6) : '123456'
const passwordHash = await bcrypt.hash(defaultPassword, 10)
const emp = await tx.employee.create({
data: {
orgId,
@@ -260,6 +264,7 @@ export async function createEmployee(orgId: string, userId: string, data: any) {
gender: data.gender,
femaleWorkerType: data.femaleWorkerType,
phone: data.phone,
passwordHash,
idCardNumber: data.idCardNumber ? encrypt(data.idCardNumber) : null,
idCardHash: data.idCardNumber ? sha256(data.idCardNumber) : null,
isPregnant: data.isPregnant || false,