feat: 商业保险/员工福利独立页面+易签宝电子签署框架
- 商业保险:从社公商保中拆出为独立页面,侧边栏新增「福利保障」分组 - 员工福利:新建完整模块(方案管理+批量参保+员工汇总),Prisma模型+后端路由+前端页面 - 电子签署:搭建易签宝对接框架(ESignRecord模型+创建/查询/取消/回调接口+前端签署管理页面) - 侧边栏新增「福利保障」分组:商业保险、员工福利、电子签署 - Prisma schema 新增6个模型:CommercialInsurancePlan/Enrollment, EmployeeBenefitPlan/Enrollment, ESignRecord
This commit is contained in:
@@ -183,6 +183,11 @@ model Organization {
|
||||
attendancePublishes AttendancePublish[]
|
||||
specialStatuses EmployeeSpecialStatus[]
|
||||
companyFiles CompanyFile[]
|
||||
commercialInsPlans CommercialInsurancePlan[]
|
||||
commercialInsEnrollments CommercialInsuranceEnrollment[]
|
||||
benefitPlans EmployeeBenefitPlan[]
|
||||
benefitEnrollments EmployeeBenefitEnrollment[]
|
||||
eSignRecords ESignRecord[]
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -268,6 +273,9 @@ model Employee {
|
||||
calendarEvents CalendarEvent[]
|
||||
workProcesses WorkProcess[]
|
||||
specialStatuses EmployeeSpecialStatus[]
|
||||
commercialInsEnrollments CommercialInsuranceEnrollment[]
|
||||
benefitEnrollments EmployeeBenefitEnrollment[]
|
||||
eSignRecords ESignRecord[]
|
||||
|
||||
@@unique([orgId, idCardHash])
|
||||
}
|
||||
@@ -1365,3 +1373,114 @@ model AcceptanceTest {
|
||||
@@unique([orgId, verifierName])
|
||||
@@index([orgId, status])
|
||||
}
|
||||
|
||||
// ========== 商业保险 ==========
|
||||
model CommercialInsurancePlan {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
type String // ACCIDENT | SUPPLEMENTARY_MEDICAL | EMPLOYER_LIABILITY | CRITICAL_ILLNESS | GROUP_LIFE | OTHER
|
||||
provider String // 保险公司
|
||||
policyNo String?
|
||||
premium Float // 年保费
|
||||
coverageAmount Float // 保额
|
||||
effectiveFrom String // YYYY-MM-DD
|
||||
effectiveTo String? // null = 长期
|
||||
description String?
|
||||
status String @default("ACTIVE") // ACTIVE | EXPIRED | CANCELLED
|
||||
enrollments CommercialInsuranceEnrollment[]
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([orgId, status])
|
||||
@@index([orgId, type])
|
||||
}
|
||||
|
||||
model CommercialInsuranceEnrollment {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
planId String
|
||||
plan CommercialInsurancePlan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
||||
employeeId String
|
||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
premium Float // 个人保费
|
||||
effectiveFrom String // YYYY-MM-DD
|
||||
effectiveTo String?
|
||||
status String @default("ACTIVE") // ACTIVE | TERMINATED
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([orgId, planId])
|
||||
@@index([employeeId])
|
||||
}
|
||||
|
||||
// ========== 员工福利 ==========
|
||||
model EmployeeBenefitPlan {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
category String // TRANSPORT | MEAL | HOUSING | COMMUNICATION | HEALTH_CHECK | HOLIDAY | BIRTHDAY | OTHER
|
||||
amount Float // 每月金额(或每次金额)
|
||||
frequency String @default("MONTHLY") // MONTHLY | QUARTERLY | YEARLY | ONE_TIME
|
||||
taxDeductible Boolean @default(false) // 是否税前扣除
|
||||
description String?
|
||||
status String @default("ACTIVE")
|
||||
enrollments EmployeeBenefitEnrollment[]
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([orgId, status])
|
||||
@@index([orgId, category])
|
||||
}
|
||||
|
||||
model EmployeeBenefitEnrollment {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
planId String
|
||||
plan EmployeeBenefitPlan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
||||
employeeId String
|
||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
effectiveFrom String // YYYY-MM
|
||||
effectiveTo String? // null = 至今
|
||||
amount Float? // 覆盖默认金额(个别调整)
|
||||
status String @default("ACTIVE")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([orgId, planId])
|
||||
@@index([employeeId])
|
||||
}
|
||||
|
||||
// ========== 电子签署(易签宝) ==========
|
||||
model ESignRecord {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
contractId String? // 关联 LaborContract
|
||||
employeeId String
|
||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
flowId String? // 易签宝流程ID
|
||||
documentTitle String // 文件标题
|
||||
documentContent String? // 文件内容(HTML/PDF base64)
|
||||
status String @default("PENDING") // PENDING | SIGNING | COMPLETED | REJECTED | EXPIRED | CANCELLED
|
||||
signUrl String? // 签署链接
|
||||
signedPdfUrl String? // 签署完成后的PDF链接
|
||||
initiatedBy String // 发起人(HR用户ID)
|
||||
completedAt DateTime?
|
||||
expiredAt DateTime?
|
||||
callbackData Json? // 易签宝回调数据
|
||||
remark String?
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([orgId, status])
|
||||
@@index([employeeId])
|
||||
@@index([contractId])
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ import companyFileRoutes from './routes/company-file.routes'
|
||||
import acceptanceTestRoutes from './routes/acceptance-test.routes'
|
||||
import leaveRoutes from './routes/leave.routes'
|
||||
import salaryRoutes from './routes/salary.routes'
|
||||
import commercialInsuranceRoutes from './routes/commercial-insurance.routes'
|
||||
import benefitRoutes from './routes/benefit.routes'
|
||||
import esignRoutes from './routes/esign.routes'
|
||||
app.use('/api/v1/auth', authRoutes)
|
||||
app.use('/api/v1/dashboard', dashboardRoutes)
|
||||
app.use('/api/v1/employees', employeeRoutes)
|
||||
@@ -95,6 +98,9 @@ app.use('/api/v1/company-files', companyFileRoutes)
|
||||
app.use('/api/v1/acceptance-tests', acceptanceTestRoutes)
|
||||
app.use('/api/v1/leaves', leaveRoutes)
|
||||
app.use('/api/v1/salary', salaryRoutes)
|
||||
app.use('/api/v1/commercial-insurance', commercialInsuranceRoutes)
|
||||
app.use('/api/v1/benefits', benefitRoutes)
|
||||
app.use('/api/v1/esign', esignRoutes)
|
||||
|
||||
app.use(errorHandler)
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
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 planSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
category: z.string(),
|
||||
amount: z.number().default(0),
|
||||
frequency: z.string().default('MONTHLY'),
|
||||
taxDeductible: z.boolean().default(false),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
const BENEFIT_CATEGORIES: Record<string, string> = {
|
||||
TRANSPORT: '交通补贴',
|
||||
MEAL: '餐补',
|
||||
HOUSING: '住房补贴',
|
||||
COMMUNICATION: '通讯补贴',
|
||||
HEALTH_CHECK: '体检',
|
||||
HOLIDAY: '节日福利',
|
||||
BIRTHDAY: '生日福利',
|
||||
OTHER: '其他',
|
||||
}
|
||||
|
||||
// 福利方案列表
|
||||
router.get('/plans', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const plans = await prisma.employeeBenefitPlan.findMany({
|
||||
where: { orgId: req.user!.orgId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: { _count: { select: { enrollments: { where: { status: 'ACTIVE' } } } } },
|
||||
})
|
||||
res.json({ success: true, data: plans })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 创建福利方案
|
||||
router.post('/plans', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = planSchema.parse(req.body)
|
||||
const plan = await prisma.employeeBenefitPlan.create({
|
||||
data: { ...data, orgId: req.user!.orgId, createdBy: req.user!.id },
|
||||
})
|
||||
res.json({ success: true, data: plan })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 更新福利方案
|
||||
router.put('/plans/:planId', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = planSchema.partial().parse(req.body)
|
||||
const plan = await prisma.employeeBenefitPlan.update({
|
||||
where: { id: req.params.planId, orgId: req.user!.orgId },
|
||||
data,
|
||||
})
|
||||
res.json({ success: true, data: plan })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 删除福利方案
|
||||
router.delete('/plans/:planId', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await prisma.employeeBenefitPlan.delete({
|
||||
where: { id: req.params.planId, orgId: req.user!.orgId },
|
||||
})
|
||||
res.json({ success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 方案参保人员
|
||||
router.get('/plans/:planId/enrollments', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const enrollments = await prisma.employeeBenefitEnrollment.findMany({
|
||||
where: { orgId: req.user!.orgId, planId: req.params.planId },
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
const data = enrollments.map((e: any) => ({
|
||||
id: e.id,
|
||||
employeeId: e.employeeId,
|
||||
name: e.employee.name,
|
||||
department: e.employee.department,
|
||||
effectiveFrom: e.effectiveFrom,
|
||||
effectiveTo: e.effectiveTo,
|
||||
amount: e.amount,
|
||||
status: e.status,
|
||||
}))
|
||||
res.json({ success: true, data })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 批量参保
|
||||
router.post('/plans/:planId/enroll', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { employeeIds, effectiveFrom } = req.body as { employeeIds: string[]; effectiveFrom: string }
|
||||
const plan = await prisma.employeeBenefitPlan.findFirst({ where: { id: req.params.planId, orgId: req.user!.orgId } })
|
||||
if (!plan) return res.status(404).json({ success: false, error: { message: '方案不存在' } })
|
||||
|
||||
const existing = await prisma.employeeBenefitEnrollment.findMany({
|
||||
where: { planId: plan.id, employeeId: { in: employeeIds }, status: 'ACTIVE' },
|
||||
select: { employeeId: true },
|
||||
})
|
||||
const existingIds = new Set(existing.map((e: any) => e.employeeId))
|
||||
const newIds = employeeIds.filter((id) => !existingIds.has(id))
|
||||
|
||||
if (newIds.length > 0) {
|
||||
await prisma.employeeBenefitEnrollment.createMany({
|
||||
data: newIds.map((empId) => ({
|
||||
orgId: req.user!.orgId,
|
||||
planId: plan.id,
|
||||
employeeId: empId,
|
||||
effectiveFrom: effectiveFrom || new Date().toISOString().slice(0, 7),
|
||||
})),
|
||||
})
|
||||
}
|
||||
res.json({ success: true, data: { enrolled: newIds.length, skipped: existingIds.size } })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 退福利
|
||||
router.post('/enrollments/:enrollmentId/terminate', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { effectiveTo } = req.body as { effectiveTo: string }
|
||||
const enrollment = await prisma.employeeBenefitEnrollment.update({
|
||||
where: { id: req.params.enrollmentId, orgId: req.user!.orgId },
|
||||
data: { status: 'TERMINATED', effectiveTo: effectiveTo || new Date().toISOString().slice(0, 7) },
|
||||
})
|
||||
res.json({ success: true, data: enrollment })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 员工福利汇总(按员工维度)
|
||||
router.get('/employee-summary', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const enrollments = await prisma.employeeBenefitEnrollment.findMany({
|
||||
where: { orgId: req.user!.orgId, status: 'ACTIVE' },
|
||||
include: {
|
||||
employee: { select: { id: true, name: true, department: true } },
|
||||
plan: { select: { id: true, name: true, category: true, amount: true } },
|
||||
},
|
||||
})
|
||||
const summary: Record<string, any> = {}
|
||||
for (const e of enrollments) {
|
||||
if (!summary[e.employeeId]) {
|
||||
summary[e.employeeId] = {
|
||||
employeeId: e.employeeId,
|
||||
name: e.employee.name,
|
||||
department: e.employee.department,
|
||||
benefits: [],
|
||||
totalMonthly: 0,
|
||||
}
|
||||
}
|
||||
const amount = e.amount ?? e.plan.amount
|
||||
summary[e.employeeId].benefits.push({
|
||||
planId: e.planId,
|
||||
planName: e.plan.name,
|
||||
category: e.plan.category,
|
||||
categoryLabel: BENEFIT_CATEGORIES[e.plan.category] || e.plan.category,
|
||||
amount,
|
||||
})
|
||||
if (e.plan.frequency === 'MONTHLY') {
|
||||
summary[e.employeeId].totalMonthly += amount
|
||||
}
|
||||
}
|
||||
res.json({ success: true, data: Object.values(summary) })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,132 @@
|
||||
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 planSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
type: z.string(),
|
||||
provider: z.string().min(1),
|
||||
policyNo: z.string().optional(),
|
||||
premium: z.number().default(0),
|
||||
coverageAmount: z.number().default(0),
|
||||
effectiveFrom: z.string(),
|
||||
effectiveTo: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
// 方案列表
|
||||
router.get('/plans', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const plans = await prisma.commercialInsurancePlan.findMany({
|
||||
where: { orgId: req.user!.orgId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: { _count: { select: { enrollments: { where: { status: 'ACTIVE' } } } } },
|
||||
})
|
||||
res.json({ success: true, data: plans })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 方案详情(含参保人员)
|
||||
router.get('/plans/:planId/enrollments', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const enrollments = await prisma.commercialInsuranceEnrollment.findMany({
|
||||
where: { orgId: req.user!.orgId, planId: req.params.planId },
|
||||
include: {
|
||||
employee: { select: { id: true, name: true, department: true, idCardNumber: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
const data = enrollments.map((e: any) => ({
|
||||
id: e.id,
|
||||
employeeId: e.employeeId,
|
||||
name: e.employee.name,
|
||||
department: e.employee.department,
|
||||
idCardMasked: e.employee.idCardNumber ? e.employee.idCardNumber.slice(0, 3) + '****' + e.employee.idCardNumber.slice(-4) : null,
|
||||
premium: e.premium,
|
||||
effectiveFrom: e.effectiveFrom,
|
||||
effectiveTo: e.effectiveTo,
|
||||
status: e.status,
|
||||
}))
|
||||
res.json({ success: true, data })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 创建方案
|
||||
router.post('/plans', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = planSchema.parse(req.body)
|
||||
const plan = await prisma.commercialInsurancePlan.create({
|
||||
data: { ...data, orgId: req.user!.orgId, createdBy: req.user!.id },
|
||||
})
|
||||
res.json({ success: true, data: plan })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 更新方案
|
||||
router.put('/plans/:planId', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = planSchema.partial().parse(req.body)
|
||||
const plan = await prisma.commercialInsurancePlan.update({
|
||||
where: { id: req.params.planId, orgId: req.user!.orgId },
|
||||
data,
|
||||
})
|
||||
res.json({ success: true, data: plan })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 删除方案
|
||||
router.delete('/plans/:planId', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await prisma.commercialInsurancePlan.delete({
|
||||
where: { id: req.params.planId, orgId: req.user!.orgId },
|
||||
})
|
||||
res.json({ success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 批量参保
|
||||
router.post('/plans/:planId/enroll', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { employeeIds, premium, effectiveFrom } = req.body as { employeeIds: string[]; premium: number; effectiveFrom: string }
|
||||
const plan = await prisma.commercialInsurancePlan.findFirst({ where: { id: req.params.planId, orgId: req.user!.orgId } })
|
||||
if (!plan) return res.status(404).json({ success: false, error: { message: '方案不存在' } })
|
||||
|
||||
const existing = await prisma.commercialInsuranceEnrollment.findMany({
|
||||
where: { planId: plan.id, employeeId: { in: employeeIds }, status: 'ACTIVE' },
|
||||
select: { employeeId: true },
|
||||
})
|
||||
const existingIds = new Set(existing.map((e: any) => e.employeeId))
|
||||
const newIds = employeeIds.filter((id) => !existingIds.has(id))
|
||||
|
||||
if (newIds.length > 0) {
|
||||
await prisma.commercialInsuranceEnrollment.createMany({
|
||||
data: newIds.map((empId) => ({
|
||||
orgId: req.user!.orgId,
|
||||
planId: plan.id,
|
||||
employeeId: empId,
|
||||
premium: premium || plan.premium,
|
||||
effectiveFrom: effectiveFrom || plan.effectiveFrom,
|
||||
})),
|
||||
})
|
||||
}
|
||||
res.json({ success: true, data: { enrolled: newIds.length, skipped: existingIds.size } })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 退保
|
||||
router.post('/enrollments/:enrollmentId/terminate', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { effectiveTo } = req.body as { effectiveTo: string }
|
||||
const enrollment = await prisma.commercialInsuranceEnrollment.update({
|
||||
where: { id: req.params.enrollmentId, orgId: req.user!.orgId },
|
||||
data: { status: 'TERMINATED', effectiveTo: effectiveTo || new Date().toISOString().slice(0, 10) },
|
||||
})
|
||||
res.json({ success: true, data: enrollment })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,149 @@
|
||||
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(),
|
||||
})
|
||||
|
||||
// 签署记录列表
|
||||
router.get('/', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const status = req.query.status as string | undefined
|
||||
const records = await prisma.eSignRecord.findMany({
|
||||
where: {
|
||||
orgId: req.user!.orgId,
|
||||
...(status && { status }),
|
||||
},
|
||||
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,
|
||||
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
|
||||
Reference in New Issue
Block a user