9512b555ee
- ESignRecord 模型新增 scene 字段(CONTRACT/RESIGNATION/POLICY/PAYSLIP/ONBOARDING) - Organization 模型新增3个电子签开关:esignPolicyEnabled/esignPayslipEnabled/esignOnboardingEnabled - 设置页面企业信息新增电子签署设置区域,3个开关各自独立,缺省关闭 - 规章制度签收:开启电子签后,员工阅读确认时自动创建POLICY场景签署记录 - 工资条确认:开启电子签后,员工确认工资条时自动创建PAYSLIP场景签署记录 - 入职文件签署:开启电子签后,HR审批通过入职流程时自动创建ONBOARDING场景签署记录 - 电子签署列表增加场景筛选下拉(全部场景/劳动合同/离职协议/规章制度/工资条/入职文件) - 管理端和员工端列表均展示场景标签(基于scene字段,替代硬编码判断) - 合同和离职流程的esign调用已加scene参数
154 lines
5.0 KiB
TypeScript
154 lines
5.0 KiB
TypeScript
import { Router, Response, NextFunction } from 'express'
|
|
import { z } from 'zod'
|
|
import prisma from '../lib/prisma'
|
|
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
|
|
|
const router = Router()
|
|
router.use(authMiddleware)
|
|
|
|
const createSignSchema = z.object({
|
|
contractId: z.string().optional(),
|
|
employeeId: z.string().min(1),
|
|
documentTitle: z.string().min(1),
|
|
documentContent: z.string().optional(),
|
|
remark: z.string().optional(),
|
|
scene: z.string().optional(),
|
|
})
|
|
|
|
// 签署记录列表
|
|
router.get('/', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const status = req.query.status as string | undefined
|
|
const scene = req.query.scene as string | undefined
|
|
const records = await prisma.eSignRecord.findMany({
|
|
where: {
|
|
orgId: req.user!.orgId,
|
|
...(status && { status }),
|
|
...(scene && { scene }),
|
|
},
|
|
include: {
|
|
employee: { select: { id: true, name: true, department: true, phone: true } },
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
})
|
|
res.json({ success: true, data: records })
|
|
} catch (err) { next(err) }
|
|
})
|
|
|
|
// 发起签署
|
|
// TODO: 对接易签宝 API — 此处为框架预留
|
|
// 1. 调用易签宝创建签署流程
|
|
// 2. 获取签署链接
|
|
// 3. 保存 flowId 和 signUrl
|
|
router.post('/create', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const data = createSignSchema.parse(req.body)
|
|
|
|
// 预留:调用易签宝 API 创建签署流程
|
|
// const esignResult = await esignApi.createFlow({
|
|
// title: data.documentTitle,
|
|
// signerPhone: employee.phone,
|
|
// signerName: employee.name,
|
|
// content: data.documentContent,
|
|
// })
|
|
// const flowId = esignResult.flowId
|
|
// const signUrl = esignResult.signUrl
|
|
|
|
const record = await prisma.eSignRecord.create({
|
|
data: {
|
|
orgId: req.user!.orgId,
|
|
contractId: data.contractId || null,
|
|
employeeId: data.employeeId,
|
|
scene: data.scene || 'CONTRACT',
|
|
documentTitle: data.documentTitle,
|
|
documentContent: data.documentContent || null,
|
|
// flowId: esignResult.flowId, // TODO: 易签宝对接后启用
|
|
// signUrl: esignResult.signUrl, // TODO: 易签宝对接后启用
|
|
status: 'PENDING',
|
|
initiatedBy: req.user!.id,
|
|
createdBy: req.user!.id,
|
|
remark: data.remark || null,
|
|
expiredAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30天过期
|
|
},
|
|
})
|
|
|
|
res.json({
|
|
success: true,
|
|
data: record,
|
|
message: '签署记录已创建。对接易签宝API后,将自动生成签署链接并发送给员工。',
|
|
})
|
|
} catch (err) { next(err) }
|
|
})
|
|
|
|
// 查询签署状态
|
|
// TODO: 对接易签宝 API — 查询流程状态并同步
|
|
router.get('/:id/status', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const record = await prisma.eSignRecord.findFirst({
|
|
where: { id: req.params.id, orgId: req.user!.orgId },
|
|
})
|
|
if (!record) return res.status(404).json({ success: false, error: { message: '记录不存在' } })
|
|
|
|
// 预留:调用易签宝 API 查询流程状态
|
|
// const esignStatus = await esignApi.getFlowStatus(record.flowId)
|
|
// if (esignStatus !== record.status) {
|
|
// await prisma.eSignRecord.update({ where: { id: record.id }, data: { status: esignStatus } })
|
|
// }
|
|
|
|
res.json({ success: true, data: record })
|
|
} catch (err) { next(err) }
|
|
})
|
|
|
|
// 易签宝回调接口(无需认证)
|
|
router.post('/callback', async (req, res: Response, next: NextFunction) => {
|
|
try {
|
|
const { flowId, status, signedPdfUrl, ...rest } = req.body
|
|
|
|
// TODO: 验证易签宝回调签名
|
|
// if (!verifyEsignCallback(req.headers, req.body)) {
|
|
// return res.status(401).json({ success: false, error: { message: '无效回调' } })
|
|
// }
|
|
|
|
if (flowId) {
|
|
const record = await prisma.eSignRecord.findFirst({ where: { flowId } })
|
|
if (record) {
|
|
await prisma.eSignRecord.update({
|
|
where: { id: record.id },
|
|
data: {
|
|
status: status || 'COMPLETED',
|
|
signedPdfUrl: signedPdfUrl || null,
|
|
completedAt: status === 'COMPLETED' ? new Date() : null,
|
|
callbackData: rest as any,
|
|
},
|
|
})
|
|
|
|
// 如果关联了合同,更新合同的签署方式和电子合同URL
|
|
if (record.contractId && status === 'COMPLETED') {
|
|
await prisma.laborContract.update({
|
|
where: { id: record.contractId },
|
|
data: {
|
|
signMethod: 'ELECTRONIC',
|
|
electronicContractUrl: signedPdfUrl || null,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
res.json({ success: true })
|
|
} catch (err) { next(err) }
|
|
})
|
|
|
|
// 取消签署
|
|
router.post('/:id/cancel', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const record = await prisma.eSignRecord.update({
|
|
where: { id: req.params.id, orgId: req.user!.orgId },
|
|
data: { status: 'CANCELLED' },
|
|
})
|
|
res.json({ success: true, data: record })
|
|
} catch (err) { next(err) }
|
|
})
|
|
|
|
export default router
|