feat: 平台管理员端 — SUPER_ADMIN 角色 + 企业租户管理 + 用户管理 + 数据总览

This commit is contained in:
selfrelease
2026-07-29 11:49:48 +08:00
parent fb36b10402
commit 987a4678f7
17 changed files with 1744 additions and 14 deletions
+11 -1
View File
@@ -1,6 +1,6 @@
import { Router } from 'express'
import { registerSchema, loginSchema, refreshSchema, resetPasswordSchema, forgotPasswordSchema, verifyCodeSchema } from '../schemas/auth.schema'
import { register, login, refresh, resetPassword } from '../services/auth.service'
import { register, login, refresh, resetPassword, platformLogin } from '../services/auth.service'
import { authLimiter, loginLimiter } from '../middleware/rateLimit'
import prisma from '../lib/prisma'
import bcrypt from 'bcryptjs'
@@ -28,6 +28,16 @@ router.post('/login', loginLimiter, async (req, res, next) => {
}
})
router.post('/platform-login', loginLimiter, async (req, res, next) => {
try {
const data = loginSchema.parse(req.body)
const result = await platformLogin(data.phone, data.password)
res.json({ success: true, data: result })
} catch (err) {
next(err)
}
})
router.post('/refresh', async (req, res, next) => {
try {
const data = refreshSchema.parse(req.body)
+337
View File
@@ -0,0 +1,337 @@
/**
* 平台管理员路由
* 管理所有企业租户、用户、数据总览
* 所有接口需要 SUPER_ADMIN 权限
*/
import { Router } from 'express'
import prisma from '../lib/prisma'
import { AuthRequest, authMiddleware, platformAdminMiddleware } from '../middleware/auth'
import { loginLimiter } from '../middleware/rateLimit'
const router = Router()
// 所有平台路由都需要认证 + SUPER_ADMIN 权限
router.use(authMiddleware, platformAdminMiddleware)
// ========== 数据总览 ==========
/**
* 平台总览数据
*/
router.get('/dashboard', async (_req: AuthRequest, res, next) => {
try {
const [orgs, users, employees, contracts, payslips] = await Promise.all([
prisma.organization.count(),
prisma.user.count({ where: { role: { not: 'SUPER_ADMIN' } } }),
prisma.employee.count(),
prisma.laborContract.count(),
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,
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 = parseInt(req.query.page as string) || 1
const pageSize = parseInt(req.query.pageSize as string) || 20
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,
payrollFrequency: 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.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 } = req.body as {
name?: string; plan?: string; maxEmployees?: number;
city?: string; contactName?: string; contactPhone?: string
}
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.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 = parseInt(req.query.page as string) || 1
const pageSize = parseInt(req.query.pageSize as string) || 20
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 } = 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 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