优化: 大文件拆分+代码分割+按需加载+console清理+any类型替换

- Money.tsx (2260行→54行): 拆分为 money/ 子目录4个组件, React.lazy二级分割
- AIAssistant.tsx (2038行→63行): 拆分为 ai-assistant/ 子目录6个组件, React.lazy二级分割
- xlsx改为动态导入, OvertimeTab从345KB降至12.7KB
- api-services.ts: 请求参数 any→Record<string,unknown>
- 移除前端3处console.log残留
- 后端console替换为pino logger
- 前后端未使用import/变量清理
- Zod schema验证: termination/platform/special-status/work-process
- 新增 leave.routes.ts, acceptance-test.routes.ts
- UI组件: PageGuide, QueryError, Stepper
This commit is contained in:
freedakgmail
2026-08-04 07:53:37 +08:00
parent 1da385cd5d
commit 2968484d2d
109 changed files with 8950 additions and 5926 deletions
+6 -1
View File
@@ -5,6 +5,7 @@ import morgan from 'morgan'
import compression from 'compression'
import { errorHandler } from './middleware/errorHandler'
import { apiLimiter } from './middleware/rateLimit'
import logger from './lib/logger'
const app = express()
@@ -62,6 +63,8 @@ import workProcessRoutes from './routes/work-process.routes'
import enterpriseTemplateRoutes from './routes/enterprise-template.routes'
import specialStatusRoutes from './routes/special-status.routes'
import companyFileRoutes from './routes/company-file.routes'
import acceptanceTestRoutes from './routes/acceptance-test.routes'
import leaveRoutes from './routes/leave.routes'
app.use('/api/v1/auth', authRoutes)
app.use('/api/v1/dashboard', dashboardRoutes)
app.use('/api/v1/employees', employeeRoutes)
@@ -88,13 +91,15 @@ app.use('/api/v1/work-processes', workProcessRoutes)
app.use('/api/v1/enterprise-templates', enterpriseTemplateRoutes)
app.use('/api/v1/special-statuses', specialStatusRoutes)
app.use('/api/v1/company-files', companyFileRoutes)
app.use('/api/v1/acceptance-tests', acceptanceTestRoutes)
app.use('/api/v1/leaves', leaveRoutes)
app.use(errorHandler)
// RAG 知识库自动初始化(异步,不阻塞启动)
import { seedKnowledgeBase } from './services/rag.service'
seedKnowledgeBase().catch((err) => {
console.warn('[RAG] 知识库初始化失败,AI 问答将不使用 RAG 检索:', err?.message || err)
logger.warn({ err }, 'RAG 知识库初始化失败,AI 问答将不使用 RAG 检索')
})
export default app
+2 -1
View File
@@ -1,10 +1,11 @@
import app from './app'
import { validateEnv } from './lib/config'
import logger from './lib/logger'
validateEnv()
const PORT = Number(process.env.PORT) || 3000
app.listen(PORT, '::', () => {
console.log(`Server running on http://[::]:${PORT}`)
logger.info(`Server running on http://[::]:${PORT}`)
})
+9 -7
View File
@@ -7,6 +7,8 @@
* 功能:存储、验证、过期清理、失败次数限制
*/
import logger from './logger'
interface CodeEntry {
code: string
expiresAt: number
@@ -28,11 +30,11 @@ async function getRedis() {
try {
const { createClient } = await import('redis')
redisClient = createClient({ url: REDIS_URL })
redisClient.on('error', (err: any) => console.error('[Redis] error:', err))
redisClient.on('error', (err: any) => logger.error({ err }, 'Redis error'))
await redisClient.connect()
console.log('[Redis] 验证码存储已连接')
logger.info('Redis 验证码存储已连接')
} catch (err) {
console.warn('[Redis] 连接失败,降级为内存存储:', err)
logger.warn({ err }, 'Redis 连接失败,降级为内存存储')
return null
}
}
@@ -59,7 +61,7 @@ export async function setCode(key: string, code: string, ttlMs: number = 5 * 60
await redis.set(`${KEY_PREFIX}${key}`, JSON.stringify(entry), { PX: ttlMs })
return
} catch (err) {
console.warn('[Redis] set 失败,降级内存:', err)
logger.warn({ err }, 'Redis set 失败,降级内存')
}
}
memoryStore.set(key, entry)
@@ -81,7 +83,7 @@ export async function getCode(key: string): Promise<CodeEntry | null> {
}
return entry
} catch (err) {
console.warn('[Redis] get 失败,降级内存:', err)
logger.warn({ err }, 'Redis get 失败,降级内存')
}
}
@@ -108,7 +110,7 @@ export async function updateCode(key: string, updates: Partial<CodeEntry>): Prom
}
return
} catch (err) {
console.warn('[Redis] update 失败,降级内存:', err)
logger.warn({ err }, 'Redis update 失败,降级内存')
}
}
@@ -128,7 +130,7 @@ export async function deleteCode(key: string): Promise<void> {
await redis.del(`${KEY_PREFIX}${key}`)
return
} catch (err) {
console.warn('[Redis] del 失败,降级内存:', err)
logger.warn({ err }, 'Redis del 失败,降级内存')
}
}
memoryStore.delete(key)
+4 -2
View File
@@ -1,3 +1,5 @@
import logger from './logger'
const required = ['JWT_SECRET', 'JWT_REFRESH_SECRET', 'ENCRYPTION_KEY']
const defaults: Record<string, string> = {
JWT_SECRET: 'dev-secret',
@@ -13,10 +15,10 @@ export function validateEnv(): void {
}
}
if (missing.length > 0 && process.env.NODE_ENV === 'production') {
console.error(`[FATAL] 以下环境变量未设置或使用了默认值,生产环境禁止启动: ${missing.join(', ')}`)
logger.fatal({ missing }, '以下环境变量未设置或使用了默认值,生产环境禁止启动')
process.exit(1)
}
if (missing.length > 0) {
console.warn(`[WARN] 以下环境变量使用了默认值,仅限开发环境: ${missing.join(', ')}`)
logger.warn({ missing }, '以下环境变量使用了默认值,仅限开发环境')
}
}
+23
View File
@@ -0,0 +1,23 @@
import pino from 'pino'
const isDev = process.env.NODE_ENV !== 'production'
const logger = pino({
level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),
transport: isDev
? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss',
ignore: 'pid,hostname',
},
}
: undefined,
redact: {
paths: ['req.headers.authorization', 'req.headers.cookie', '*.password', '*.passwordHash', '*.token', '*.refreshToken'],
censor: '[REDACTED]',
},
})
export default logger
+8
View File
@@ -0,0 +1,8 @@
/**
* 统一分页参数解析,全局限制 pageSize ≤ 200,防止过量数据查询
*/
export function parsePagination(query: Record<string, any>): { page: number; pageSize: number } {
const page = Math.max(1, parseInt(query.page as string) || 1)
const pageSize = Math.min(Math.max(1, parseInt(query.pageSize as string) || 20), 200)
return { page, pageSize }
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { AuthRequest } from './auth'
import prisma from '../lib/prisma'
import logger from '../lib/logger'
export async function auditLog(
req: AuthRequest,
@@ -22,6 +23,6 @@ export async function auditLog(
},
})
} catch (err) {
console.error('Audit log error:', err)
logger.error({ err, action, entity, entityId }, 'Audit log error')
}
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { Request, Response, NextFunction } from 'express'
import { ZodError } from 'zod'
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'
import { AppError } from '../lib/AppError'
import logger from '../lib/logger'
export function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction) {
if (err instanceof AppError) {
@@ -55,7 +56,7 @@ export function errorHandler(err: unknown, _req: Request, res: Response, _next:
})
}
console.error('Unhandled error:', err)
logger.error({ err }, 'Unhandled error')
return res.status(500).json({
success: false,
error: { code: 'INTERNAL_ERROR', message: '服务器内部错误' },
@@ -0,0 +1,142 @@
import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import prisma from '../lib/prisma'
const router = Router()
// 保存(创建或更新)
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { verifierName, results, remarks, conclusionName, conclusionDate, conclusionResult, conclusionIssues, screenshots, signature1, signature2 } = req.body
if (!verifierName || !verifierName.trim()) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '请输入验收人姓名' } })
}
const orgId = req.user!.orgId
const data = {
verifierName: verifierName.trim(),
results: results || {},
remarks: remarks || {},
conclusionName: conclusionName || null,
conclusionDate: conclusionDate || null,
conclusionResult: conclusionResult || null,
conclusionIssues: conclusionIssues || null,
screenshots: screenshots || undefined,
signature1: signature1 || undefined,
signature2: signature2 || undefined,
}
const record = await prisma.acceptanceTest.upsert({
where: { orgId_verifierName: { orgId, verifierName: verifierName.trim() } },
create: { ...data, orgId, createdBy: req.user!.id },
update: data,
})
res.json({ success: true, data: record })
} catch (err) {
next(err)
}
})
// 获取单个验收记录
router.get('/:verifierName', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const record = await prisma.acceptanceTest.findUnique({
where: {
orgId_verifierName: { orgId: req.user!.orgId, verifierName: req.params.verifierName },
},
})
if (!record) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '未找到保存记录' } })
}
res.json({ success: true, data: record })
} catch (err) {
next(err)
}
})
// 列表(所有验收记录摘要)
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const records = await prisma.acceptanceTest.findMany({
where: { orgId: req.user!.orgId },
select: {
id: true,
verifierName: true,
status: true,
conclusionResult: true,
conclusionDate: true,
updatedAt: true,
results: true,
},
orderBy: { updatedAt: 'desc' },
})
const list = records.map((r: any) => {
const vals = Object.values(r.results as any)
const pass = vals.filter((v: any) => v === 'pass').length
const total = vals.length
const tested = vals.filter((v: any) => v !== 'untested').length
return {
id: r.id,
verifierName: r.verifierName,
status: r.status,
conclusionResult: r.conclusionResult,
conclusionDate: r.conclusionDate,
updatedAt: r.updatedAt,
testedCount: tested,
passCount: pass,
totalCount: total,
}
})
res.json({ success: true, data: list })
} catch (err) {
next(err)
}
})
// 提交验收报告
router.post('/:verifierName/submit', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { conclusionResult, conclusionIssues } = req.body
const record = await prisma.acceptanceTest.findUnique({
where: {
orgId_verifierName: { orgId: req.user!.orgId, verifierName: req.params.verifierName },
},
})
if (!record) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '请先保存再提交' } })
}
const updated = await prisma.acceptanceTest.update({
where: { id: record.id },
data: {
status: 'SUBMITTED',
submittedAt: new Date(),
conclusionResult: conclusionResult || record.conclusionResult,
conclusionIssues: conclusionIssues || record.conclusionIssues,
},
})
res.json({ success: true, data: updated })
} catch (err) {
next(err)
}
})
// 删除
router.delete('/:verifierName', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
await prisma.acceptanceTest.delete({
where: {
orgId_verifierName: { orgId: req.user!.orgId, verifierName: req.params.verifierName },
},
})
res.json({ success: true })
} catch (err) {
next(err)
}
})
export default router
-1
View File
@@ -2,7 +2,6 @@ import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { z } from 'zod'
import {
createAttendanceConfirmation,
batchCreateAttendanceConfirmations,
getAttendanceConfirmations,
confirmAttendance,
+1 -1
View File
@@ -1,7 +1,7 @@
import { Router, Response, NextFunction } from 'express'
import prisma from '../lib/prisma'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { getDashboardData, getMonthlyCalendar, getCostAnalysis, getComplianceScore, getHealthCheck, saveHealthCheckReport, getHealthCheckHistory, getAnnualValueReport, saveAnnualValueReport, getAnnualValueReportHistory } from '../services/risk.service'
import { getDashboardData, getMonthlyCalendar, getCostAnalysis, getHealthCheck, saveHealthCheckReport, getHealthCheckHistory, getAnnualValueReport, saveAnnualValueReport, getAnnualValueReportHistory } from '../services/risk.service'
import { z } from 'zod'
const router = Router()
@@ -1,7 +1,6 @@
import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import prisma from '../lib/prisma'
import { renderTemplate } from '../services/template.service'
const router = Router()
-1
View File
@@ -255,7 +255,6 @@ router.get('/roster', authMiddleware, async (req: AuthRequest, res: Response, ne
const search = req.query.search as string | undefined
const status = req.query.status as string | undefined
const department = req.query.department as string | undefined
const contractStatus = req.query.contractStatus as string | undefined
const where: any = { orgId }
if (department) where.department = department
+1 -2
View File
@@ -324,7 +324,7 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
})
result.employees++
result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'success', message: '导入成功' })
result.details.push({ sheet: '员工信息', row: i + 2, name, employeeId: emp.id, status: 'success', message: '导入成功' })
} catch (e: any) {
const msg = e?.message || ''
if (msg.includes('Unique constraint')) {
@@ -720,7 +720,6 @@ router.post('/payroll', authMiddleware, upload.single('file'), async (req: AuthR
try {
if (!req.file) return res.status(400).json({ success: false, message: '请上传文件' })
const orgId = req.user!.orgId
const userId = req.user!.id
const batchId = req.body.batchId as string
if (!batchId) return res.status(400).json({ success: false, message: '缺少批次ID' })
+198
View File
@@ -0,0 +1,198 @@
import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import prisma from '../lib/prisma'
import { parsePagination } from '../lib/pagination'
const router = Router()
// 休假申请列表
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const { status, leaveType, employeeId } = req.query
const { page, pageSize } = parsePagination(req.query)
const where: any = { orgId }
if (status) where.status = status
if (leaveType) where.leaveType = leaveType
if (employeeId) where.employeeId = employeeId
const total = await prisma.leaveRequest.count({ where })
const list = await prisma.leaveRequest.findMany({
where,
include: {
employee: { select: { id: true, name: true, department: true, position: true } },
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
})
res.json({ success: true, data: { list, total, page, pageSize } })
} catch (err) {
next(err)
}
})
// 创建休假申请
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const { employeeId, leaveType, startDate, endDate, days, reason, attachment } = req.body
if (!employeeId || !leaveType || !startDate || !endDate) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少必填字段' } })
}
const record = await prisma.leaveRequest.create({
data: {
orgId,
employeeId,
leaveType,
startDate: new Date(startDate),
endDate: new Date(endDate),
days: days || 1,
reason: reason || null,
attachment: attachment || null,
createdBy: req.user!.id,
},
include: {
employee: { select: { id: true, name: true, department: true, position: true } },
},
})
res.json({ success: true, data: record })
} catch (err) {
next(err)
}
})
// 审批(批准/驳回)
router.post('/:id/approve', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
const { action, remark } = req.body // action: 'APPROVED' | 'REJECTED'
const orgId = req.user!.orgId
if (!['APPROVED', 'REJECTED'].includes(action)) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '无效的审批操作' } })
}
const existing = await prisma.leaveRequest.findFirst({ where: { id, orgId } })
if (!existing) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '申请不存在' } })
}
if (existing.status !== 'PENDING') {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '该申请已处理' } })
}
const updated = await prisma.leaveRequest.update({
where: { id },
data: {
status: action,
approverId: req.user!.id,
approvedAt: new Date(),
approveRemark: remark || null,
},
include: {
employee: { select: { id: true, name: true, department: true, position: true } },
},
})
// 如果批准,自动创建 LeaveRecord
if (action === 'APPROVED') {
await prisma.leaveRecord.create({
data: {
orgId,
employeeId: existing.employeeId,
leaveType: existing.leaveType,
startDate: existing.startDate,
endDate: existing.endDate,
days: existing.days,
reason: existing.reason || '',
remark: `休假审批通过(申请ID: ${id}`,
createdBy: req.user!.id,
},
})
}
res.json({ success: true, data: updated })
} catch (err) {
next(err)
}
})
// 撤回申请
router.post('/:id/cancel', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
const orgId = req.user!.orgId
const existing = await prisma.leaveRequest.findFirst({ where: { id, orgId } })
if (!existing) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '申请不存在' } })
}
if (existing.status !== 'PENDING') {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已处理的申请不可撤回' } })
}
const updated = await prisma.leaveRequest.update({
where: { id },
data: { status: 'CANCELLED' },
})
res.json({ success: true, data: updated })
} catch (err) {
next(err)
}
})
// 删除申请(仅待审批状态可删)
router.delete('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
const orgId = req.user!.orgId
const existing = await prisma.leaveRequest.findFirst({ where: { id, orgId } })
if (!existing) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '申请不存在' } })
}
if (existing.status !== 'PENDING') {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已处理的申请不可删除' } })
}
await prisma.leaveRequest.delete({ where: { id } })
res.json({ success: true })
} catch (err) {
next(err)
}
})
// 统计
router.get('/stats', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const { month } = req.query
const where: any = { orgId }
if (month) {
const start = new Date(`${month}-01`)
const end = new Date(`${month}-31T23:59:59`)
where.startDate = { gte: start, lte: end }
}
const [pending, approved, rejected, cancelled, total] = await Promise.all([
prisma.leaveRequest.count({ where: { ...where, status: 'PENDING' } }),
prisma.leaveRequest.count({ where: { ...where, status: 'APPROVED' } }),
prisma.leaveRequest.count({ where: { ...where, status: 'REJECTED' } }),
prisma.leaveRequest.count({ where: { ...where, status: 'CANCELLED' } }),
prisma.leaveRequest.count({ where }),
])
res.json({ success: true, data: { pending, approved, rejected, cancelled, total } })
} catch (err) {
next(err)
}
})
export default router
+7 -6
View File
@@ -132,9 +132,13 @@ router.post('/check-contracts', async (req: AuthRequest, res: Response, next: Ne
})
// 测试通知渠道
const testChannelSchema = z.object({
channel: z.enum(['wechat', 'email']),
})
router.post('/test', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { channel } = req.body as { channel: 'wechat' | 'email' }
const { channel } = testChannelSchema.parse(req.body)
const setting = await prisma.notificationSetting.findUnique({ where: { orgId: req.user!.orgId } })
if (!setting) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '通知设置不存在' } })
@@ -154,12 +158,9 @@ router.post('/test', async (req: AuthRequest, res: Response, next: NextFunction)
} catch (e: any) {
res.json({ success: false, error: { code: 'TEST_FAILED', message: `发送失败: ${e?.message || '网络错误'}` } })
}
} else if (channel === 'email') {
if (!setting.email) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '未配置通知邮箱' } })
// 邮件发送(开发阶段仅返回成功)
res.json({ success: true, data: { message: `测试邮件已发送到 ${setting.email}` } })
} else {
res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '不支持的通知渠道' } })
if (!setting.email) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '未配置通知邮箱' } })
res.json({ success: true, data: { message: `测试邮件已发送到 ${setting.email}` } })
}
} catch (err) {
next(err)
+39 -1
View File
@@ -504,7 +504,45 @@ router.put('/batches/:batchId/entries/:employeeId', async (req: AuthRequest, res
},
})
res.json({ success: true, data: updated })
res.json({ success: true, data: updated, taxBreakdown: calcResult.taxBreakdown })
} catch (err) {
next(err)
}
})
// 获取条目个税计算明细
router.get('/batches/:batchId/entries/:employeeId/tax-detail', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { batchId, employeeId } = req.params
const orgId = req.user!.orgId
const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
const entry = await prisma.batchEntry.findUnique({
where: { batchId_employeeId: { batchId, employeeId } },
})
if (!entry) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '条目不存在' } })
const inputs = {
baseSalary: entry.baseSalary,
overtimePay: entry.overtimePay,
allowance: entry.allowance,
deduction: entry.deduction,
bonus: entry.bonus,
}
const calcResult = await calcBatchEntry(orgId, employeeId, batch.month, inputs, batch.type)
const taxBreakdown = calcResult.taxBreakdown || {}
// 加入社保公积金系统计算值 vs 实际值对比
taxBreakdown.systemSocialEmp = calcResult.systemSocialEmp
taxBreakdown.systemSocialOrg = calcResult.systemSocialOrg
taxBreakdown.systemHousingEmp = calcResult.systemHousingEmp
taxBreakdown.systemHousingOrg = calcResult.systemHousingOrg
taxBreakdown.actualSocialEmp = entry.socialEmp
taxBreakdown.actualSocialOrg = entry.socialOrg
taxBreakdown.actualHousingEmp = entry.housingEmp
taxBreakdown.actualHousingOrg = entry.housingOrg
res.json({ success: true, data: taxBreakdown })
} catch (err) {
next(err)
}
+8 -30
View File
@@ -6,7 +6,8 @@
import { Router } from 'express'
import prisma from '../lib/prisma'
import { AuthRequest, authMiddleware, platformAdminMiddleware } from '../middleware/auth'
import { loginLimiter } from '../middleware/rateLimit'
import { parsePagination } from '../lib/pagination'
import { createOrgSchema, updateOrgSchema, updateOrgAdminSchema, createPlatformAdminSchema } from '../schemas/platform.schema'
const router = Router()
@@ -88,8 +89,7 @@ router.get('/dashboard', async (_req: AuthRequest, res, next) => {
*/
router.get('/orgs', async (req: AuthRequest, res, next) => {
try {
const page = parseInt(req.query.page as string) || 1
const pageSize = parseInt(req.query.pageSize as string) || 20
const { page, pageSize } = parsePagination(req.query)
const search = (req.query.search as string) || ''
const planFilter = (req.query.plan as string) || ''
@@ -154,19 +154,7 @@ router.post('/orgs', async (req: AuthRequest, res, next) => {
const {
name, plan, maxEmployees, city, contactName, contactPhone,
adminName, adminPhone, adminPassword,
} = req.body as {
name: string; plan?: string; maxEmployees?: number
city?: string; contactName?: string; contactPhone?: string
adminName?: string; adminPhone: string; adminPassword: string
}
if (!name || !adminPhone || !adminPassword) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '企业名称、管理员手机号、密码不能为空' } })
}
if (adminPassword.length < 8) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '密码至少8位' } })
}
} = createOrgSchema.parse(req.body)
const existing = await prisma.user.findUnique({ where: { phone: adminPhone } })
if (existing) {
@@ -244,10 +232,7 @@ router.get('/orgs/:id', async (req: AuthRequest, res, next) => {
*/
router.put('/orgs/:id', async (req: AuthRequest, res, next) => {
try {
const { name, plan, maxEmployees, city, contactName, contactPhone } = req.body as {
name?: string; plan?: string; maxEmployees?: number;
city?: string; contactName?: string; contactPhone?: string
}
const { name, plan, maxEmployees, city, contactName, contactPhone } = updateOrgSchema.parse(req.body)
const updateData: any = {}
if (name) updateData.name = name
@@ -274,9 +259,7 @@ router.put('/orgs/:id', async (req: AuthRequest, res, next) => {
*/
router.put('/orgs/:id/admin', async (req: AuthRequest, res, next) => {
try {
const { adminName, adminPhone, adminPassword } = req.body as {
adminName?: string; adminPhone?: string; adminPassword?: string
}
const { adminName, adminPhone, adminPassword } = updateOrgAdminSchema.parse(req.body)
// 找到该企业的 ADMIN 角色用户(第一个管理员)
const admin = await prisma.user.findFirst({
@@ -335,8 +318,7 @@ router.delete('/orgs/:id', async (req: AuthRequest, res, next) => {
*/
router.get('/users', async (req: AuthRequest, res, next) => {
try {
const page = parseInt(req.query.page as string) || 1
const pageSize = parseInt(req.query.pageSize as string) || 20
const { page, pageSize } = parsePagination(req.query)
const search = (req.query.search as string) || ''
const orgId = (req.query.orgId as string) || ''
@@ -432,11 +414,7 @@ router.get('/admins', async (_req: AuthRequest, res, next) => {
*/
router.post('/admins', async (req: AuthRequest, res, next) => {
try {
const { name, phone, password } = req.body as { name: string; phone: string; password: string }
if (!name || !phone || !password) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '姓名、手机号、密码不能为空' } })
}
const { name, phone, password } = createPlatformAdminSchema.parse(req.body)
const existing = await prisma.user.findUnique({ where: { phone } })
if (existing) {
+60
View File
@@ -838,4 +838,64 @@ router.post('/resignation/:id/withdraw', portalAuth, async (req: any, res, next)
} catch (err) { next(err) }
})
// ========== 员工端:休假申请 ==========
// 查看自己的休假申请列表
router.get('/leaves', portalAuth, async (req: any, res, next) => {
try {
const { id: employeeId, orgId } = req.employee
const list = await prisma.leaveRequest.findMany({
where: { employeeId, orgId },
orderBy: { createdAt: 'desc' },
})
res.json({ success: true, data: list })
} catch (err) { next(err) }
})
// 提交休假申请
router.post('/leaves', portalAuth, async (req: any, res, next) => {
try {
const { id: employeeId, orgId } = req.employee
const { leaveType, startDate, endDate, days, reason } = req.body
if (!leaveType || !startDate || !endDate) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少必填字段' } })
}
const record = await prisma.leaveRequest.create({
data: {
orgId,
employeeId,
leaveType,
startDate: new Date(startDate),
endDate: new Date(endDate),
days: days || 1,
reason: reason || null,
createdBy: employeeId,
},
})
res.json({ success: true, data: record })
} catch (err) { next(err) }
})
// 撤回休假申请(仅待审批可撤回)
router.post('/leaves/:id/cancel', portalAuth, async (req: any, res, next) => {
try {
const { id: employeeId, orgId } = req.employee
const record = await prisma.leaveRequest.findFirst({
where: { id: req.params.id, employeeId, orgId },
})
if (!record) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '申请不存在' } })
}
if (record.status !== 'PENDING') {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已处理的申请不可撤回' } })
}
const updated = await prisma.leaveRequest.update({
where: { id: record.id },
data: { status: 'CANCELLED' },
})
res.json({ success: true, data: updated })
} catch (err) { next(err) }
})
export default router
+7 -5
View File
@@ -5,6 +5,7 @@
import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import prisma from '../lib/prisma'
import { parsePagination } from '../lib/pagination'
import {
createSpecialStatus,
updateSpecialStatus,
@@ -13,6 +14,7 @@ import {
STATUS_TYPES,
getAlertLevel,
} from '../services/special-status.service'
import { createSpecialStatusSchema, updateSpecialStatusSchema } from '../schemas/special-status.schema'
const router = Router()
@@ -22,8 +24,7 @@ const router = Router()
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const page = parseInt(req.query.page as string) || 1
const pageSize = parseInt(req.query.pageSize as string) || 20
const { page, pageSize } = parsePagination(req.query)
const type = (req.query.type as string) || ''
const status = (req.query.status as string) || ''
const search = (req.query.search as string) || ''
@@ -97,7 +98,8 @@ router.get('/:id', authMiddleware, async (req: AuthRequest, res: Response, next:
*/
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const record = await createSpecialStatus(req.user!.orgId, req.user!.id, req.body)
const data = createSpecialStatusSchema.parse(req.body)
const record = await createSpecialStatus(req.user!.orgId, req.user!.id, data)
res.json({ success: true, data: record })
} catch (err: any) {
if (err.code) {
@@ -112,7 +114,8 @@ router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: N
*/
router.put('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const record = await updateSpecialStatus(req.user!.orgId, req.user!.id, req.params.id, req.body)
const data = updateSpecialStatusSchema.parse(req.body)
const record = await updateSpecialStatus(req.user!.orgId, req.user!.id, req.params.id, data)
res.json({ success: true, data: record })
} catch (err: any) {
if (err.code) {
@@ -167,7 +170,6 @@ router.get('/stats/overview', authMiddleware, async (req: AuthRequest, res: Resp
])
// 预警统计
const now = new Date()
const soon7 = new Date()
soon7.setDate(soon7.getDate() + 7)
const soon30 = new Date()
+13 -24
View File
@@ -1,7 +1,7 @@
import { Router } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { auditLog } from '../middleware/auditLog'
import { terminationChecklistSchema } from '../schemas/termination.schema'
import { terminationChecklistSchema, resignationSchema, batchTerminatePreviewSchema, batchTerminateSchema, createTerminationDraftSchema, updateTerminationDraftSchema } from '../schemas/termination.schema'
import { createTermination, createResignation, revokeTermination, getTerminations, getChecklistForReason, assessRisk, batchTerminatePreview, batchTerminate, createDraft, updateDraft, submitForApproval, approveTermination, rejectTermination, executeTermination, cancelTermination, getDrafts, getTerminationDetail, getDefaultHandoverItems, validateTerminationStep } from '../services/termination.service'
import prisma from '../lib/prisma'
import { createEvidence } from '../services/evidence.service'
@@ -80,10 +80,7 @@ router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
router.post('/resignation', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { employeeId, terminationDate, resignationReason, remark } = req.body
if (!employeeId || !terminationDate) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '缺少必填字段' } })
}
const { employeeId, terminationDate, resignationReason, remark } = resignationSchema.parse(req.body)
const result = await createResignation(req.user!.orgId, req.user!.id, { employeeId, terminationDate, resignationReason, remark })
await auditLog(req, 'RESIGN', 'EMPLOYEE', employeeId, { resignationReason })
res.json({ success: true, data: result })
@@ -120,12 +117,7 @@ router.delete('/:id/revoke', authMiddleware, async (req: AuthRequest, res, next)
// 批量解聘预检
router.post('/batch/preview', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { items } = req.body as {
items: Array<{ employeeId: string; reason: string; terminationDate: string }>
}
if (!items || !Array.isArray(items) || items.length === 0) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 items' } })
}
const { items } = batchTerminatePreviewSchema.parse(req.body)
const results = await batchTerminatePreview(req.user!.orgId, items)
res.json({ success: true, data: { total: results.length, warnings: results.filter(r => r.warnings.length > 0).length, results } })
} catch (err) {
@@ -136,12 +128,7 @@ router.post('/batch/preview', authMiddleware, async (req: AuthRequest, res, next
// 批量解聘执行
router.post('/batch', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { items } = req.body as {
items: Array<{ employeeId: string; reason: string; terminationDate: string; compensation?: number }>
}
if (!items || !Array.isArray(items) || items.length === 0) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 items' } })
}
const { items } = batchTerminateSchema.parse(req.body)
const result = await batchTerminate(req.user!.orgId, req.user!.id, items)
for (const id of result.success) {
await auditLog(req, 'TERMINATE', 'EMPLOYEE', id, { batch: true })
@@ -192,15 +179,16 @@ router.get('/handover-template', authMiddleware, async (req: AuthRequest, res) =
// 创建草稿
router.post('/draft', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const result = await createDraft(req.user!.orgId, req.user!.id, req.body)
const emp = await prisma.employee.findFirst({ where: { id: req.body.employeeId }, select: { name: true, department: true } })
const data = createTerminationDraftSchema.parse(req.body)
const result = await createDraft(req.user!.orgId, req.user!.id, data)
const emp = await prisma.employee.findFirst({ where: { id: data.employeeId }, select: { name: true, department: true } })
await auditLog(req, 'CREATE_DRAFT', 'TERMINATION_RECORD', result.id, {
employeeName: emp?.name || '',
department: emp?.department || '',
reason: req.body.reason || '',
type: req.body.type || 'TERMINATION',
terminationDate: req.body.terminationDate || '',
compensation: req.body.compensation || 0,
reason: data.reason || '',
type: data.type || 'TERMINATION',
terminationDate: data.terminationDate || '',
compensation: data.compensation || 0,
})
res.json({ success: true, data: result })
} catch (err: any) {
@@ -214,7 +202,8 @@ router.post('/draft', authMiddleware, async (req: AuthRequest, res, next) => {
// 更新草稿
router.put('/draft/:id', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const result = await updateDraft(req.user!.orgId, req.params.id, req.user!.id, req.body)
const data = updateTerminationDraftSchema.parse(req.body)
const result = await updateDraft(req.user!.orgId, req.params.id, req.user!.id, data)
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
+13 -12
View File
@@ -1,17 +1,17 @@
import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import prisma from '../lib/prisma'
import { parsePagination } from '../lib/pagination'
import { executeWorkProcess, generateDocument, PROCESS_TYPES, PROCESS_STATUS } from '../services/work-process.service'
import { createWorkProcessSchema, updateWorkProcessSchema } from '../schemas/work-process.schema'
const router = Router()
// 创建办理(含草稿)
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { type, title, employeeId, formData, status = 'DRAFT', remark } = req.body
if (!type || !PROCESS_TYPES[type]) {
return res.status(400).json({ success: false, error: { code: 'INVALID_TYPE', message: '无效的流程类型' } })
}
const data = createWorkProcessSchema.parse(req.body)
const { type, title, employeeId, formData, status, remark } = data
const process = await (prisma as any).workProcess.create({
data: {
orgId: req.user!.orgId,
@@ -33,7 +33,8 @@ router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: N
// 列表查询
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { type, status, page = '1', pageSize = '20' } = req.query
const { type, status } = req.query
const { page, pageSize } = parsePagination(req.query)
const where: any = { orgId: req.user!.orgId }
if (type) where.type = type
if (status) where.status = status
@@ -42,10 +43,10 @@ router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: Ne
where,
include: { employee: { select: { id: true, name: true, department: true } } },
orderBy: { createdAt: 'desc' },
skip: (Number(page) - 1) * Number(pageSize),
take: Number(pageSize),
skip: (page - 1) * pageSize,
take: pageSize,
})
res.json({ success: true, data: { items, total, page: Number(page), pageSize: Number(pageSize) } })
res.json({ success: true, data: { items, total, page, pageSize } })
} catch (err) {
next(err)
}
@@ -79,7 +80,7 @@ router.patch('/:id', authMiddleware, async (req: AuthRequest, res: Response, nex
if (existing.status !== 'DRAFT') {
return res.status(400).json({ success: false, error: { code: 'NOT_DRAFT', message: '仅草稿状态可编辑' } })
}
const { title, employeeId, formData, remark } = req.body
const { title, employeeId, formData, remark } = updateWorkProcessSchema.parse(req.body)
const updated = await (prisma as any).workProcess.update({
where: { id: req.params.id },
data: {
@@ -116,7 +117,7 @@ router.post('/:id/submit', authMiddleware, async (req: AuthRequest, res: Respons
}
// 生成文书
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } })
const doc = generateDocument(process.type, process.formData, org?.name || '')
const doc = await generateDocument(process.type, process.formData, org?.name || '')
const documents = doc.content ? [doc] : []
const updated = await (prisma as any).workProcess.update({
where: { id: process.id },
@@ -151,7 +152,7 @@ router.post('/:id/approve', authMiddleware, async (req: AuthRequest, res: Respon
return res.status(400).json({ success: false, error: { code: 'EXEC_FAILED', message: `执行失败:${execErr?.message || '未知错误'}` } })
}
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } })
const doc = generateDocument(process.type, process.formData, org?.name || '')
const doc = await generateDocument(process.type, process.formData, org?.name || '')
const documents = doc.content ? [doc] : []
const updated = await (prisma as any).workProcess.update({
where: { id: process.id },
@@ -247,7 +248,7 @@ router.get('/:id/preview', authMiddleware, async (req: AuthRequest, res: Respons
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } })
}
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } })
const doc = generateDocument(process.type, process.formData, org?.name || '')
const doc = await generateDocument(process.type, process.formData, org?.name || '')
res.json({ success: true, data: doc })
} catch (err) {
next(err)
+34
View File
@@ -0,0 +1,34 @@
import { z } from 'zod'
export const createOrgSchema = z.object({
name: z.string().min(2, '企业名称至少2个字').max(50, '企业名称最多50个字'),
plan: z.enum(['FREE', 'PRO', 'ENTERPRISE']).default('FREE'),
maxEmployees: z.number().int().min(1).max(100000).default(20),
city: z.string().max(50).optional(),
contactName: z.string().max(30).optional(),
contactPhone: z.string().regex(/^1[3-9]\d{9}$/).optional(),
adminName: z.string().max(30).optional(),
adminPhone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'),
adminPassword: z.string().min(8, '密码至少8位').max(32, '密码最多32位'),
})
export const updateOrgSchema = z.object({
name: z.string().min(2).max(50).optional(),
plan: z.enum(['FREE', 'PRO', 'ENTERPRISE']).optional(),
maxEmployees: z.number().int().min(1).max(100000).optional(),
city: z.string().max(50).optional().nullable(),
contactName: z.string().max(30).optional().nullable(),
contactPhone: z.string().regex(/^1[3-9]\d{9}$/).optional().nullable(),
})
export const updateOrgAdminSchema = z.object({
adminName: z.string().max(30).optional(),
adminPhone: z.string().regex(/^1[3-9]\d{9}$/).optional(),
adminPassword: z.string().min(8, '密码至少8位').max(32).optional(),
})
export const createPlatformAdminSchema = z.object({
name: z.string().min(1, '姓名不能为空').max(30),
phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'),
password: z.string().min(8, '密码至少8位').max(32, '密码最多32位'),
})
@@ -0,0 +1,40 @@
import { z } from 'zod'
export const createSpecialStatusSchema = z.object({
employeeId: z.string().min(1, '员工ID不能为空'),
type: z.enum([
'PREGNANCY', 'WORK_INJURY', 'MEDICAL_PERIOD', 'OTHER',
], { errorMap: () => ({ message: '无效的特殊状态类型' }) }),
status: z.enum(['ACTIVE', 'PENDING', 'RESOLVED']).default('ACTIVE'),
startDate: z.string().optional().nullable(),
endDate: z.string().optional().nullable(),
expectedDueDate: z.string().optional().nullable(),
injuryDate: z.string().optional().nullable(),
injuryDescription: z.string().max(500).optional().nullable(),
certificationDate: z.string().optional().nullable(),
certificationNo: z.string().max(50).optional().nullable(),
disabilityLevel: z.string().max(20).optional().nullable(),
assessmentDate: z.string().optional().nullable(),
medicalMonths: z.number().int().min(1).max(36).optional().nullable(),
description: z.string().max(500).optional().nullable(),
attachments: z.array(z.any()).optional().nullable(),
reminderDate: z.string().optional().nullable(),
})
export const updateSpecialStatusSchema = z.object({
type: z.enum(['PREGNANCY', 'WORK_INJURY', 'MEDICAL_PERIOD', 'OTHER']).optional(),
status: z.enum(['ACTIVE', 'PENDING', 'RESOLVED']).optional(),
startDate: z.string().optional().nullable(),
endDate: z.string().optional().nullable(),
expectedDueDate: z.string().optional().nullable(),
injuryDate: z.string().optional().nullable(),
injuryDescription: z.string().max(500).optional().nullable(),
certificationDate: z.string().optional().nullable(),
certificationNo: z.string().max(50).optional().nullable(),
disabilityLevel: z.string().max(20).optional().nullable(),
assessmentDate: z.string().optional().nullable(),
medicalMonths: z.number().int().min(1).max(36).optional().nullable(),
description: z.string().max(500).optional().nullable(),
attachments: z.array(z.any()).optional().nullable(),
reminderDate: z.string().optional().nullable(),
})
+42
View File
@@ -13,3 +13,45 @@ export const terminationQuerySchema = z.object({
page: z.coerce.number().min(1).default(1),
pageSize: z.coerce.number().min(1).max(50).default(20),
})
export const resignationSchema = z.object({
employeeId: z.string().min(1, '员工ID不能为空'),
terminationDate: z.string().min(1, '离职日期不能为空'),
resignationReason: z.string().max(200).optional(),
remark: z.string().max(500).optional(),
})
export const batchTerminatePreviewSchema = z.object({
items: z.array(z.object({
employeeId: z.string().min(1),
reason: z.string().min(1, '解聘原因不能为空'),
terminationDate: z.string().min(1, '解聘日期不能为空'),
})).min(1, '至少选择一名员工'),
})
export const batchTerminateSchema = z.object({
items: z.array(z.object({
employeeId: z.string().min(1),
reason: z.string().min(1, '解聘原因不能为空'),
terminationDate: z.string().min(1, '解聘日期不能为空'),
compensation: z.number().min(0).optional(),
})).min(1, '至少选择一名员工'),
})
export const createTerminationDraftSchema = z.object({
employeeId: z.string().min(1, '员工ID不能为空'),
type: z.enum(['TERMINATION', 'RESIGNATION']).optional(),
reason: z.string().max(200).optional(),
terminationDate: z.string().optional(),
compensation: z.number().min(0).optional(),
handoverItems: z.array(z.any()).optional(),
remark: z.string().max(500).optional(),
})
export const updateTerminationDraftSchema = z.object({
reason: z.string().max(200).optional(),
terminationDate: z.string().optional(),
compensation: z.number().min(0).optional(),
handoverItems: z.array(z.any()).optional(),
remark: z.string().max(500).optional(),
})
@@ -0,0 +1,21 @@
import { z } from 'zod'
export const createWorkProcessSchema = z.object({
type: z.enum([
'HIRE', 'ONBOARD', 'CUSTOM_CONTRACT', 'INFO_SUBMIT', 'CONFIRM',
'CHANGE', 'RENEW', 'SUSPEND', 'INCOME_CERT', 'TERMINATE',
'RESCIND', 'LEAVING_CERT', 'FLEXIBLE',
]),
title: z.string().max(100).optional(),
employeeId: z.string().optional().nullable(),
formData: z.record(z.any()).optional(),
status: z.enum(['DRAFT', 'COMPLETED']).default('DRAFT'),
remark: z.string().max(500).optional().nullable(),
})
export const updateWorkProcessSchema = z.object({
title: z.string().max(100).optional(),
employeeId: z.string().optional().nullable(),
formData: z.record(z.any()).optional(),
remark: z.string().max(500).optional().nullable(),
})
+28 -1
View File
@@ -205,6 +205,12 @@ export async function calcBatchEntry(
housingOrg = Math.max(0, fullHousingOrg - deductedHousingOrg)
}
// 保存系统计算值(覆盖前)
const systemSocialEmp = socialEmp
const systemSocialOrg = socialOrg
const systemHousingEmp = housingEmp
const systemHousingOrg = housingOrg
// 手动覆盖社保值
if (options?.overrideSocial) {
if (options.overrideSocial.socialEmp !== undefined) socialEmp = options.overrideSocial.socialEmp
@@ -229,9 +235,11 @@ export async function calcBatchEntry(
// 个税计算
let tax = 0
let taxBreakdown: any = null
if (batchType === 'BONUS') {
// 年终奖单独计税
tax = calcBonusTax(inputs.bonus)
taxBreakdown = { method: '单独计税(年终奖)', bonus: inputs.bonus, tax }
} else {
// 累计预扣法(补偿金也走累计预扣,但无社保公积金扣除)
const year = month.slice(0, 4)
@@ -252,8 +260,22 @@ export async function calcBatchEntry(
const ytdHousingEmp = archivedEntries.reduce((s, e) => s + e.housingEmp, 0) + housingEmp
const ytdSpecialDeduction = employee.specialDeduction * Number(month.slice(5, 7))
const ytdTaxDeducted = archivedEntries.reduce((s, e) => s + e.tax, 0)
const ytdTaxableIncome = Math.max(0, ytdIncome - 5000 * Number(month.slice(5, 7)) - ytdSocialEmp - ytdHousingEmp - ytdSpecialDeduction)
const deductionAmount = 5000 * Number(month.slice(5, 7))
const ytdTaxableIncome = Math.max(0, ytdIncome - deductionAmount - ytdSocialEmp - ytdHousingEmp - ytdSpecialDeduction)
tax = calcCumulativeTax(ytdTaxableIncome, ytdTaxDeducted)
taxBreakdown = {
method: '累计预扣法',
month: Number(month.slice(5, 7)),
ytdIncome,
deductionAmount,
ytdSocialEmp,
ytdHousingEmp,
ytdSpecialDeduction,
ytdTaxableIncome,
ytdTaxDeducted,
currentMonthTax: tax,
archivedCount: archivedEntries.length,
}
}
const netPay = totalPay - socialEmp - housingEmp - tax
@@ -263,7 +285,12 @@ export async function calcBatchEntry(
socialOrg: Math.round(socialOrg * 100) / 100,
housingEmp: Math.round(housingEmp * 100) / 100,
housingOrg: Math.round(housingOrg * 100) / 100,
systemSocialEmp: Math.round(systemSocialEmp * 100) / 100,
systemSocialOrg: Math.round(systemSocialOrg * 100) / 100,
systemHousingEmp: Math.round(systemHousingEmp * 100) / 100,
systemHousingOrg: Math.round(systemHousingOrg * 100) / 100,
tax,
taxBreakdown,
totalPay: Math.round(totalPay * 100) / 100,
netPay: Math.round(netPay * 100) / 100,
}
+2 -11
View File
@@ -666,7 +666,7 @@ export async function getDashboardData(orgId: string) {
const yearEnd = new Date(now.getFullYear(), 11, 31, 23, 59, 59)
const [
employeeCount, highRisks, pendingRisks, riskItems, resolvedItems,
employeeCount, _highRisks, _pendingRisks, riskItems, resolvedItems,
overtimeRecords, payslips, batchEntries, socialConfig, housingConfig,
monthContracts, monthTerminations, monthDisciplinary, monthAttendance,
monthSeverancePay,
@@ -1217,17 +1217,13 @@ export async function getCostAnalysis(orgId: string, month: string) {
const monthNum = parseInt(month.slice(5, 7))
// 当月数据
const currentMonthStart = new Date(year, monthNum - 1, 1)
const currentMonthEnd = new Date(year, monthNum, 0, 23, 59, 59)
// 上月(环比)
const prevMonthStart = new Date(year, monthNum - 2, 1)
const prevMonthEnd = new Date(year, monthNum - 1, 0, 23, 59, 59)
const prevMonthStr = `${prevMonthStart.getFullYear()}-${String(prevMonthStart.getMonth() + 1).padStart(2, '0')}`
// 去年同月(同比)
const lastYearMonthStart = new Date(year - 1, monthNum - 1, 1)
const lastYearMonthEnd = new Date(year - 1, monthNum, 0, 23, 59, 59)
const lastYearMonthStr = `${lastYearMonthStart.getFullYear()}-${String(lastYearMonthStart.getMonth() + 1).padStart(2, '0')}`
// 获取各月归档批次汇总(按员工去重,与 getDashboardData 口径一致)
@@ -1802,7 +1798,7 @@ export async function getAnnualValueReport(orgId: string, year: number) {
const [
risksResolved,
lossAvoidedAgg,
_lossAvoidedAgg,
aiConversations,
aiReviews,
contractsSigned,
@@ -2043,11 +2039,6 @@ export async function getAnnualValueReport(orgId: string, year: number) {
// 总价值 = 规避损失 + 节约成本
const totalValue = adjustedLossAvoided + costSaved
// ROI = 总价值 / 系统成本(年费 12000 元),上限 9999%
const systemCost = 12000
const rawRoi = systemCost > 0 ? Math.round((totalValue / systemCost) * 100) : 0
const roi = Math.min(rawRoi, 9999)
const metrics = {
risksResolved,
lossAvoided,
+21 -6
View File
@@ -1,8 +1,7 @@
import prisma from '../lib/prisma'
import { encrypt } from '../lib/crypto'
import crypto from 'crypto'
import { createDraft as createTerminationDraft, executeTermination, createResignation } from './termination.service'
import { createEmployee, addContract, batchRenew } from './contract.service'
import { createDraft as createTerminationDraft, executeTermination } from './termination.service'
import { createEmployee, addContract } from './contract.service'
import { runRiskDetection } from './risk.service'
// 13类流程定义
@@ -67,7 +66,7 @@ export async function executeWorkProcess(processId: string, type: string, formDa
return { employeeId }
}
case 'CONFIRM': {
const { employeeId, confirmDate, regularSalary } = formData
const { employeeId, regularSalary } = formData
if (employeeId) {
if (regularSalary) {
await prisma.employee.update({
@@ -196,7 +195,7 @@ export async function executeWorkProcess(processId: string, type: string, formDa
return {}
}
case 'FLEXIBLE': {
const { name, phone, idCardNumber, department, agreementStartDate, agreementEndDate, payMethod } = formData
const { name, phone, idCardNumber, department, agreementStartDate, agreementEndDate } = formData
const empResult = await createEmployee(orgId, userId, {
name,
department: department || '灵活用工',
@@ -244,7 +243,23 @@ export async function executeWorkProcess(processId: string, type: string, formDa
}
// 生成文书预览
export function generateDocument(type: string, formData: any, orgName: string): { name: string; content: string } {
export async function generateDocument(type: string, formData: any, orgName: string): Promise<{ name: string; content: string }> {
// 如果指定了企业自定义模板,使用企业模板渲染
if (formData.enterpriseTemplateId) {
const tpl = await (prisma as any).enterpriseTemplate.findFirst({
where: { id: formData.enterpriseTemplateId },
})
if (tpl) {
let content = tpl.content
// 替换变量 {{var}}
const allVars: Record<string, string> = { ...formData, companyName: orgName }
for (const [key, value] of Object.entries(allVars)) {
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value ?? ''))
}
return { name: `${tpl.name}.doc`, content }
}
}
const templates: Record<string, (data: any, org: string) => string> = {
INCOME_CERT: (data, org) => `收入证明