6ec939dc7f
- Schema: 去掉 payrollFrequency,新增 payrollDays (JSON数组) + payrollReminderDays (Int) - 设置页: 发薪日期改为1-28号多选按钮,新增提前提醒天数设置 - 设置页: 恢复电子签署设置区域(3个开关:规章制度/工资条/入职文件) - 工作日历: 发薪日期作为 PAYROLL_DAY 事件显示 - 工作台: 提前N天提醒发薪日期,N可配置 - TaskCenter: 新增发薪提醒分类图标 - seed文件: 更新为 payrollDays 格式
438 lines
12 KiB
TypeScript
438 lines
12 KiB
TypeScript
/**
|
|
* 平台管理员路由
|
|
* 管理所有企业租户、用户、数据总览
|
|
* 所有接口需要 SUPER_ADMIN 权限
|
|
*/
|
|
import { Router } from 'express'
|
|
import prisma from '../lib/prisma'
|
|
import { AuthRequest, authMiddleware, platformAdminMiddleware } from '../middleware/auth'
|
|
import { parsePagination } from '../lib/pagination'
|
|
import { createOrgSchema, updateOrgSchema, updateOrgAdminSchema, createPlatformAdminSchema } from '../schemas/platform.schema'
|
|
|
|
const router = Router()
|
|
|
|
// 所有平台路由都需要认证 + SUPER_ADMIN 权限
|
|
router.use(authMiddleware, platformAdminMiddleware)
|
|
|
|
// ========== 数据总览 ==========
|
|
|
|
/**
|
|
* 平台总览数据
|
|
*/
|
|
router.get('/dashboard', async (_req: AuthRequest, res, next) => {
|
|
try {
|
|
const now = new Date()
|
|
const [orgs, users, employees, contracts, activeContracts, payslips] = await Promise.all([
|
|
prisma.organization.count(),
|
|
prisma.user.count({ where: { role: { not: 'SUPER_ADMIN' } } }),
|
|
prisma.employee.count(),
|
|
prisma.laborContract.count(),
|
|
prisma.laborContract.count({
|
|
where: {
|
|
OR: [
|
|
{ endDate: null },
|
|
{ endDate: { gte: now } },
|
|
],
|
|
},
|
|
}),
|
|
prisma.payslip.count(),
|
|
])
|
|
|
|
// 按套餐分组
|
|
const orgsByPlan = await prisma.organization.groupBy({
|
|
by: ['plan'],
|
|
_count: true,
|
|
})
|
|
|
|
// 最近 7 天注册的企业
|
|
const recentOrgs = await prisma.organization.findMany({
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 5,
|
|
select: {
|
|
id: true, name: true, plan: true, city: true,
|
|
createdAt: true, maxEmployees: true,
|
|
_count: { select: { employees: true, users: true } },
|
|
},
|
|
})
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
totalOrgs: orgs,
|
|
totalUsers: users,
|
|
totalEmployees: employees,
|
|
totalContracts: contracts,
|
|
activeContracts,
|
|
totalPayslips: payslips,
|
|
orgsByPlan: orgsByPlan.map((g: any) => ({ plan: g.plan, count: g._count })),
|
|
recentOrgs: recentOrgs.map((o: any) => ({
|
|
id: o.id,
|
|
name: o.name,
|
|
plan: o.plan,
|
|
city: o.city,
|
|
createdAt: o.createdAt,
|
|
maxEmployees: o.maxEmployees,
|
|
employeeCount: o._count.employees,
|
|
userCount: o._count.users,
|
|
})),
|
|
},
|
|
})
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
// ========== 企业租户管理 ==========
|
|
|
|
/**
|
|
* 企业列表(分页 + 搜索)
|
|
*/
|
|
router.get('/orgs', async (req: AuthRequest, res, next) => {
|
|
try {
|
|
const { page, pageSize } = parsePagination(req.query)
|
|
const search = (req.query.search as string) || ''
|
|
const planFilter = (req.query.plan as string) || ''
|
|
|
|
const where: any = {}
|
|
if (search) {
|
|
where.OR = [
|
|
{ name: { contains: search, mode: 'insensitive' } },
|
|
{ city: { contains: search, mode: 'insensitive' } },
|
|
{ contactName: { contains: search, mode: 'insensitive' } },
|
|
{ contactPhone: { contains: search } },
|
|
]
|
|
}
|
|
if (planFilter) {
|
|
where.plan = planFilter
|
|
}
|
|
|
|
const [orgs, total] = await Promise.all([
|
|
prisma.organization.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
select: {
|
|
id: true, name: true, plan: true, maxEmployees: true,
|
|
city: true, contactName: true, contactPhone: true,
|
|
payrollDays: true, retirementReminderEnabled: true,
|
|
createdAt: true, updatedAt: true,
|
|
_count: {
|
|
select: { employees: true, users: true, contracts: true, payslips: true },
|
|
},
|
|
},
|
|
}),
|
|
prisma.organization.count({ where }),
|
|
])
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
list: orgs.map((o: any) => ({
|
|
...o,
|
|
employeeCount: o._count.employees,
|
|
userCount: o._count.users,
|
|
contractCount: o._count.contracts,
|
|
payslipCount: o._count.payslips,
|
|
_count: undefined,
|
|
})),
|
|
total,
|
|
page,
|
|
pageSize,
|
|
},
|
|
})
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 创建企业客户(含管理员账号)
|
|
*/
|
|
router.post('/orgs', async (req: AuthRequest, res, next) => {
|
|
try {
|
|
const {
|
|
name, plan, maxEmployees, city, contactName, contactPhone,
|
|
adminName, adminPhone, adminPassword,
|
|
} = createOrgSchema.parse(req.body)
|
|
|
|
const existing = await prisma.user.findUnique({ where: { phone: adminPhone } })
|
|
if (existing) {
|
|
return res.status(400).json({ success: false, error: { code: 'DUPLICATE', message: '该手机号已存在' } })
|
|
}
|
|
|
|
// 创建企业
|
|
const org = await prisma.organization.create({
|
|
data: {
|
|
name,
|
|
plan: (plan as any) || 'FREE',
|
|
maxEmployees: maxEmployees || 20,
|
|
city: city || null,
|
|
contactName: contactName || null,
|
|
contactPhone: contactPhone || null,
|
|
},
|
|
})
|
|
|
|
// 创建管理员账号
|
|
const bcrypt = await import('bcryptjs')
|
|
const passwordHash = await bcrypt.default.hash(adminPassword, 10)
|
|
const admin = await prisma.user.create({
|
|
data: {
|
|
orgId: org.id,
|
|
phone: adminPhone,
|
|
name: adminName || '管理员',
|
|
passwordHash,
|
|
role: 'ADMIN',
|
|
},
|
|
select: { id: true, name: true, phone: true, role: true },
|
|
})
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
org: { id: org.id, name: org.name, plan: org.plan, maxEmployees: org.maxEmployees, city: org.city },
|
|
admin,
|
|
},
|
|
})
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 企业详情
|
|
*/
|
|
router.get('/orgs/:id', async (req: AuthRequest, res, next) => {
|
|
try {
|
|
const org = await prisma.organization.findUnique({
|
|
where: { id: req.params.id },
|
|
include: {
|
|
_count: {
|
|
select: { employees: true, users: true, contracts: true, payslips: true },
|
|
},
|
|
users: {
|
|
select: { id: true, name: true, phone: true, role: true, disabled: true, lastLoginAt: true, createdAt: true },
|
|
orderBy: { createdAt: 'asc' },
|
|
},
|
|
},
|
|
})
|
|
|
|
if (!org) {
|
|
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '企业不存在' } })
|
|
}
|
|
|
|
res.json({ success: true, data: org })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 更新企业信息(套餐、员工上限等)
|
|
*/
|
|
router.put('/orgs/:id', async (req: AuthRequest, res, next) => {
|
|
try {
|
|
const { name, plan, maxEmployees, city, contactName, contactPhone } = updateOrgSchema.parse(req.body)
|
|
|
|
const updateData: any = {}
|
|
if (name) updateData.name = name
|
|
if (plan) updateData.plan = plan
|
|
if (maxEmployees !== undefined) updateData.maxEmployees = maxEmployees
|
|
if (city !== undefined) updateData.city = city
|
|
if (contactName !== undefined) updateData.contactName = contactName
|
|
if (contactPhone !== undefined) updateData.contactPhone = contactPhone
|
|
|
|
const org = await prisma.organization.update({
|
|
where: { id: req.params.id },
|
|
data: updateData,
|
|
select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true },
|
|
})
|
|
|
|
res.json({ success: true, data: org })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 编辑企业管理员(姓名、手机号、可选重置密码)
|
|
*/
|
|
router.put('/orgs/:id/admin', async (req: AuthRequest, res, next) => {
|
|
try {
|
|
const { adminName, adminPhone, adminPassword } = updateOrgAdminSchema.parse(req.body)
|
|
|
|
// 找到该企业的 ADMIN 角色用户(第一个管理员)
|
|
const admin = await prisma.user.findFirst({
|
|
where: { orgId: req.params.id, role: 'ADMIN' },
|
|
orderBy: { createdAt: 'asc' },
|
|
})
|
|
|
|
if (!admin) {
|
|
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '该企业未设置管理员' } })
|
|
}
|
|
|
|
// 如果修改了手机号,检查是否已被其他用户占用
|
|
if (adminPhone && adminPhone !== admin.phone) {
|
|
const existing = await prisma.user.findUnique({ where: { phone: adminPhone } })
|
|
if (existing && existing.id !== admin.id) {
|
|
return res.status(409).json({ success: false, error: { code: 'DUPLICATE_PHONE', message: '该手机号已被使用' } })
|
|
}
|
|
}
|
|
|
|
const updateData: any = {}
|
|
if (adminName) updateData.name = adminName
|
|
if (adminPhone) updateData.phone = adminPhone
|
|
if (adminPassword && adminPassword.length >= 8) {
|
|
const bcrypt = require('bcryptjs')
|
|
updateData.passwordHash = await bcrypt.hash(adminPassword, 10)
|
|
}
|
|
|
|
const updated = await prisma.user.update({
|
|
where: { id: admin.id },
|
|
data: updateData,
|
|
select: { id: true, name: true, phone: true, role: true },
|
|
})
|
|
|
|
res.json({ success: true, data: updated })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 删除企业(级联删除所有数据)
|
|
*/
|
|
router.delete('/orgs/:id', async (req: AuthRequest, res, next) => {
|
|
try {
|
|
await prisma.organization.delete({ where: { id: req.params.id } })
|
|
res.json({ success: true, data: { message: '企业已删除' } })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
// ========== 用户管理 ==========
|
|
|
|
/**
|
|
* 所有企业用户列表(分页 + 搜索)
|
|
*/
|
|
router.get('/users', async (req: AuthRequest, res, next) => {
|
|
try {
|
|
const { page, pageSize } = parsePagination(req.query)
|
|
const search = (req.query.search as string) || ''
|
|
const orgId = (req.query.orgId as string) || ''
|
|
|
|
const where: any = { role: { not: 'SUPER_ADMIN' } }
|
|
if (search) {
|
|
where.OR = [
|
|
{ name: { contains: search, mode: 'insensitive' } },
|
|
{ phone: { contains: search } },
|
|
]
|
|
}
|
|
if (orgId) {
|
|
where.orgId = orgId
|
|
}
|
|
|
|
const [users, total] = await Promise.all([
|
|
prisma.user.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
select: {
|
|
id: true, name: true, phone: true, role: true,
|
|
disabled: true, lastLoginAt: true, createdAt: true,
|
|
org: { select: { id: true, name: true } },
|
|
},
|
|
}),
|
|
prisma.user.count({ where }),
|
|
])
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
list: users.map((u: any) => ({
|
|
...u,
|
|
orgName: u.org?.name || null,
|
|
org: undefined,
|
|
})),
|
|
total,
|
|
page,
|
|
pageSize,
|
|
},
|
|
})
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 启用/禁用用户
|
|
*/
|
|
router.put('/users/:id/toggle', async (req: AuthRequest, res, next) => {
|
|
try {
|
|
const user = await prisma.user.findUnique({ where: { id: req.params.id } })
|
|
if (!user) {
|
|
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '用户不存在' } })
|
|
}
|
|
if (user.role === 'SUPER_ADMIN') {
|
|
return res.status(400).json({ success: false, error: { code: 'FORBIDDEN', message: '不能操作平台管理员账号' } })
|
|
}
|
|
|
|
const updated = await prisma.user.update({
|
|
where: { id: req.params.id },
|
|
data: { disabled: !user.disabled },
|
|
select: { id: true, name: true, phone: true, role: true, disabled: true },
|
|
})
|
|
|
|
res.json({ success: true, data: updated })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
// ========== 平台管理员管理 ==========
|
|
|
|
/**
|
|
* 平台管理员列表
|
|
*/
|
|
router.get('/admins', async (_req: AuthRequest, res, next) => {
|
|
try {
|
|
const admins = await prisma.user.findMany({
|
|
where: { role: 'SUPER_ADMIN' },
|
|
select: { id: true, name: true, phone: true, disabled: true, lastLoginAt: true, createdAt: true },
|
|
orderBy: { createdAt: 'asc' },
|
|
})
|
|
res.json({ success: true, data: admins })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 创建平台管理员
|
|
*/
|
|
router.post('/admins', async (req: AuthRequest, res, next) => {
|
|
try {
|
|
const { name, phone, password } = createPlatformAdminSchema.parse(req.body)
|
|
|
|
const existing = await prisma.user.findUnique({ where: { phone } })
|
|
if (existing) {
|
|
return res.status(400).json({ success: false, error: { code: 'DUPLICATE', message: '该手机号已存在' } })
|
|
}
|
|
|
|
const bcrypt = await import('bcryptjs')
|
|
const passwordHash = await bcrypt.default.hash(password, 10)
|
|
const admin = await prisma.user.create({
|
|
data: { name, phone, passwordHash, role: 'SUPER_ADMIN', orgId: null },
|
|
select: { id: true, name: true, phone: true, role: true },
|
|
})
|
|
|
|
res.json({ success: true, data: admin })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
export default router
|