398 lines
15 KiB
JavaScript
398 lines
15 KiB
JavaScript
import express from 'express'
|
|
import { orgs, nextOrgId, buildOrgTree, collectDescendantIds } from '../data/orgs.js'
|
|
import { users, nextUserId } from '../data/users.js'
|
|
import { roles, nextRoleId } from '../data/roles.js'
|
|
import { menuTree } from '../data/menu.js'
|
|
import { requireAuth } from '../middleware/requireAuth.js'
|
|
|
|
const router = express.Router()
|
|
router.use(requireAuth)
|
|
|
|
const DATA_SCOPES = ['own', 'ownAndSub', 'all']
|
|
|
|
function orgName(orgId) {
|
|
return orgs.find((o) => o.id === orgId)?.name || ''
|
|
}
|
|
|
|
function roleNames(roleIds) {
|
|
return (roleIds || []).map((id) => roles.find((r) => r.id === id)?.name).filter(Boolean)
|
|
}
|
|
|
|
function nowStr() {
|
|
return new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
}
|
|
|
|
function genInitialPassword() {
|
|
const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'
|
|
const lower = 'abcdefghijkmnpqrstuvwxyz'
|
|
const digit = '0123456789'
|
|
const all = upper + lower + digit
|
|
let pwd = upper[Math.floor(Math.random() * upper.length)] + digit[Math.floor(Math.random() * digit.length)]
|
|
for (let i = 0; i < 6; i += 1) pwd += all[Math.floor(Math.random() * all.length)]
|
|
return pwd
|
|
}
|
|
|
|
// ---------------- 机构管理 ----------------
|
|
|
|
router.get('/org/list', (req, res) => {
|
|
const { name, id } = req.query || {}
|
|
let list = orgs
|
|
if (name || id) {
|
|
const matched = orgs.filter(
|
|
(o) => (name && o.name.includes(name)) || (id && o.id.includes(id))
|
|
)
|
|
const idSet = new Set()
|
|
matched.forEach((m) => {
|
|
let cur = m
|
|
while (cur) {
|
|
idSet.add(cur.id)
|
|
cur = orgs.find((o) => o.id === cur.parentId)
|
|
}
|
|
})
|
|
list = orgs.filter((o) => idSet.has(o.id))
|
|
}
|
|
res.json({ code: 200, msg: 'ok', data: buildOrgTree(list) })
|
|
})
|
|
|
|
router.post('/org', (req, res) => {
|
|
const { name, parentId, remark } = req.body || {}
|
|
if (!name) return res.json({ code: 40004, msg: '机构名称不能为空', data: null })
|
|
if (!parentId) return res.json({ code: 40004, msg: '请选择上级机构', data: null })
|
|
const parent = orgs.find((o) => o.id === parentId)
|
|
if (!parent) return res.json({ code: 40004, msg: '上级机构不存在', data: null })
|
|
const duplicate = orgs.some((o) => o.parentId === parentId && o.name === name)
|
|
if (duplicate) return res.json({ code: 40005, msg: '同一上级机构下机构名称不可重复', data: null })
|
|
|
|
const org = {
|
|
id: nextOrgId(),
|
|
name,
|
|
parentId,
|
|
level: parent.level + 1,
|
|
remark: remark || '',
|
|
builtin: false,
|
|
createdAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
}
|
|
orgs.push(org)
|
|
res.json({ code: 200, msg: 'ok', data: org })
|
|
})
|
|
|
|
router.put('/org/:id', (req, res) => {
|
|
const { id } = req.params
|
|
const { name, parentId, remark } = req.body || {}
|
|
const org = orgs.find((o) => o.id === id)
|
|
if (!org) return res.json({ code: 40404, msg: '机构不存在', data: null })
|
|
if (!name) return res.json({ code: 40004, msg: '机构名称不能为空', data: null })
|
|
|
|
let nextParentId = org.parentId
|
|
let level = org.level
|
|
if (org.parentId !== null) {
|
|
if (!parentId) return res.json({ code: 40004, msg: '请选择上级机构', data: null })
|
|
if (parentId === id) return res.json({ code: 40006, msg: '上级机构不可选择自身', data: null })
|
|
const descendantIds = collectDescendantIds(orgs, id)
|
|
if (descendantIds.includes(parentId)) {
|
|
return res.json({ code: 40006, msg: '上级机构不可选择自身下级', data: null })
|
|
}
|
|
const parent = orgs.find((o) => o.id === parentId)
|
|
if (!parent) return res.json({ code: 40004, msg: '上级机构不存在', data: null })
|
|
nextParentId = parentId
|
|
level = parent.level + 1
|
|
}
|
|
|
|
const duplicate = orgs.some((o) => o.id !== id && o.parentId === nextParentId && o.name === name)
|
|
if (duplicate) return res.json({ code: 40005, msg: '同一上级机构下机构名称不可重复', data: null })
|
|
|
|
org.name = name
|
|
org.parentId = nextParentId
|
|
org.level = level
|
|
org.remark = remark || ''
|
|
res.json({ code: 200, msg: 'ok', data: org })
|
|
})
|
|
|
|
router.delete('/org/batch', (req, res) => {
|
|
const { ids } = req.body || {}
|
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
return res.json({ code: 40024, msg: '请选择要删除的机构', data: null })
|
|
}
|
|
const successIds = []
|
|
const failed = []
|
|
// 按 level 降序处理:先判定/删除层级更深的下级机构,再处理上级机构,
|
|
// 以支持"勾选整棵已选子树一次性批量删除"的自然预期行为
|
|
const sortedIds = [...ids].sort((a, b) => {
|
|
const orgA = orgs.find((o) => o.id === a)
|
|
const orgB = orgs.find((o) => o.id === b)
|
|
return (orgB?.level || 0) - (orgA?.level || 0)
|
|
})
|
|
sortedIds.forEach((id) => {
|
|
const org = orgs.find((o) => o.id === id)
|
|
if (!org) {
|
|
failed.push({ id, name: id, reason: '机构不存在' })
|
|
return
|
|
}
|
|
if (org.builtin) {
|
|
failed.push({ id, name: org.name, reason: '内置一级机构不可删除' })
|
|
return
|
|
}
|
|
const hasChildren = orgs.some((o) => o.parentId === id)
|
|
if (hasChildren) {
|
|
failed.push({ id, name: org.name, reason: '该机构存在下级机构,不可删除' })
|
|
return
|
|
}
|
|
const hasUsers = users.some((u) => u.orgId === id)
|
|
if (hasUsers) {
|
|
failed.push({ id, name: org.name, reason: '该机构已关联用户,不可删除' })
|
|
return
|
|
}
|
|
const index = orgs.findIndex((o) => o.id === id)
|
|
orgs.splice(index, 1)
|
|
successIds.push(id)
|
|
})
|
|
res.json({ code: 200, msg: 'ok', data: { successIds, failed } })
|
|
})
|
|
|
|
// ---------------- 用户管理 ----------------
|
|
|
|
router.get('/user/list', (req, res) => {
|
|
const { orgId, username, phone, status, roleId, page = 1, pageSize = 10 } = req.query || {}
|
|
let list = users
|
|
if (orgId) list = list.filter((u) => u.orgId === orgId)
|
|
if (username) list = list.filter((u) => u.username.includes(username))
|
|
if (phone) list = list.filter((u) => u.phone.includes(phone))
|
|
if (status) list = list.filter((u) => u.status === status)
|
|
if (roleId) list = list.filter((u) => u.roleIds.includes(roleId))
|
|
|
|
const total = list.length
|
|
const start = (Number(page) - 1) * Number(pageSize)
|
|
const pageList = list.slice(start, start + Number(pageSize)).map((u) => ({
|
|
id: u.id,
|
|
username: u.username,
|
|
realName: u.realName,
|
|
phone: u.phone,
|
|
orgId: u.orgId,
|
|
orgName: orgName(u.orgId),
|
|
roleIds: u.roleIds,
|
|
roleNames: roleNames(u.roleIds),
|
|
status: u.status,
|
|
createdAt: u.createdAt,
|
|
lastLoginAt: u.lastLoginAt
|
|
}))
|
|
res.json({ code: 200, msg: 'ok', data: { list: pageList, total } })
|
|
})
|
|
|
|
router.post('/user', (req, res) => {
|
|
const { username, phone, realName, orgId, roleIds } = req.body || {}
|
|
if (!username) return res.json({ code: 40010, msg: '请输入用户名', data: null })
|
|
if (!/^1\d{10}$/.test(phone || '')) return res.json({ code: 40011, msg: '请输入正确的11位手机号', data: null })
|
|
if (users.some((u) => u.phone === phone)) return res.json({ code: 40012, msg: '该手机号已被注册', data: null })
|
|
if (!orgId || !orgs.some((o) => o.id === orgId)) return res.json({ code: 40013, msg: '请选择有效的所属机构', data: null })
|
|
if (!roleIds?.length) return res.json({ code: 40014, msg: '请选择用户角色', data: null })
|
|
|
|
const initialPassword = genInitialPassword()
|
|
const user = {
|
|
id: nextUserId(),
|
|
username,
|
|
password: initialPassword,
|
|
realName: realName || '',
|
|
phone,
|
|
orgId,
|
|
roleIds,
|
|
status: 'enabled',
|
|
failCount: 0,
|
|
mustChangePassword: true,
|
|
lastLoginAt: '',
|
|
createdAt: nowStr()
|
|
}
|
|
users.push(user)
|
|
res.json({ code: 200, msg: 'ok', data: { id: user.id, initialPassword } })
|
|
})
|
|
|
|
router.put('/user/:id', (req, res) => {
|
|
const { id } = req.params
|
|
const { phone, realName, orgId, roleIds } = req.body || {}
|
|
const user = users.find((u) => u.id === id)
|
|
if (!user) return res.json({ code: 40404, msg: '用户不存在', data: null })
|
|
if (!/^1\d{10}$/.test(phone || '')) return res.json({ code: 40011, msg: '请输入正确的11位手机号', data: null })
|
|
if (users.some((u) => u.id !== id && u.phone === phone)) {
|
|
return res.json({ code: 40012, msg: '该手机号已被注册', data: null })
|
|
}
|
|
if (!orgId || !orgs.some((o) => o.id === orgId)) return res.json({ code: 40013, msg: '请选择有效的所属机构', data: null })
|
|
if (!roleIds?.length) return res.json({ code: 40014, msg: '请选择用户角色', data: null })
|
|
|
|
user.phone = phone
|
|
user.realName = realName || ''
|
|
user.orgId = orgId
|
|
user.roleIds = roleIds
|
|
res.json({ code: 200, msg: 'ok', data: null })
|
|
})
|
|
|
|
router.delete('/user/batch', (req, res) => {
|
|
const { ids } = req.body || {}
|
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
return res.json({ code: 40024, msg: '请选择要删除的用户', data: null })
|
|
}
|
|
const successIds = []
|
|
const failed = []
|
|
ids.forEach((id) => {
|
|
const user = users.find((u) => u.id === id)
|
|
if (!user) {
|
|
failed.push({ id, name: id, reason: '用户不存在' })
|
|
return
|
|
}
|
|
successIds.push(id)
|
|
})
|
|
successIds.forEach((id) => {
|
|
const index = users.findIndex((u) => u.id === id)
|
|
if (index !== -1) users.splice(index, 1)
|
|
})
|
|
res.json({ code: 200, msg: 'ok', data: { successIds, failed } })
|
|
})
|
|
|
|
router.put('/user/:id/lock', (req, res) => {
|
|
const user = users.find((u) => u.id === req.params.id)
|
|
if (!user) return res.json({ code: 40404, msg: '用户不存在', data: null })
|
|
user.status = 'locked'
|
|
res.json({ code: 200, msg: 'ok', data: null })
|
|
})
|
|
|
|
router.put('/user/:id/unlock', (req, res) => {
|
|
const user = users.find((u) => u.id === req.params.id)
|
|
if (!user) return res.json({ code: 40404, msg: '用户不存在', data: null })
|
|
user.status = 'enabled'
|
|
user.failCount = 0
|
|
res.json({ code: 200, msg: 'ok', data: null })
|
|
})
|
|
|
|
router.put('/user/:id/password', (req, res) => {
|
|
const { newPassword } = req.body || {}
|
|
const user = users.find((u) => u.id === req.params.id)
|
|
if (!user) return res.json({ code: 40404, msg: '用户不存在', data: null })
|
|
const strongEnough = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}$/.test(newPassword || '')
|
|
if (!strongEnough) return res.json({ code: 40015, msg: '密码需8位以上,且包含大小写字母和数字', data: null })
|
|
user.password = newPassword
|
|
user.mustChangePassword = false
|
|
res.json({ code: 200, msg: 'ok', data: null })
|
|
})
|
|
|
|
router.put('/user/:id/password/reset', (req, res) => {
|
|
const user = users.find((u) => u.id === req.params.id)
|
|
if (!user) return res.json({ code: 40404, msg: '用户不存在', data: null })
|
|
user.password = '123456'
|
|
user.mustChangePassword = true
|
|
console.log(`[mock-server] 密码已重置,短信通知已发送至 ${user.phone}: 初始密码 123456`)
|
|
res.json({ code: 200, msg: 'ok', data: null })
|
|
})
|
|
|
|
// ---------------- 角色管理 ----------------
|
|
|
|
router.get('/role/list', (req, res) => {
|
|
const { name, orgId, status, page = 1, pageSize = 1000 } = req.query || {}
|
|
let list = roles
|
|
if (name) list = list.filter((r) => r.name.includes(name))
|
|
if (orgId) list = list.filter((r) => r.orgId === orgId)
|
|
if (status) list = list.filter((r) => r.status === status)
|
|
|
|
const total = list.length
|
|
const start = (Number(page) - 1) * Number(pageSize)
|
|
const pageList = list.slice(start, start + Number(pageSize)).map((r) => ({
|
|
id: r.id,
|
|
name: r.name,
|
|
orgId: r.orgId,
|
|
orgName: orgName(r.orgId),
|
|
status: r.status,
|
|
builtin: r.builtin,
|
|
createdAt: r.createdAt
|
|
}))
|
|
res.json({ code: 200, msg: 'ok', data: { list: pageList, total } })
|
|
})
|
|
|
|
router.get('/role/:id', (req, res) => {
|
|
const role = roles.find((r) => r.id === req.params.id)
|
|
if (!role) return res.json({ code: 40404, msg: '角色不存在', data: null })
|
|
res.json({ code: 200, msg: 'ok', data: role })
|
|
})
|
|
|
|
router.post('/role', (req, res) => {
|
|
const { name, orgId, menuIds, buttonCodes, dataScope } = req.body || {}
|
|
if (!name) return res.json({ code: 40016, msg: '请输入角色名称', data: null })
|
|
if (!orgId || !orgs.some((o) => o.id === orgId)) return res.json({ code: 40013, msg: '请选择有效的所属机构', data: null })
|
|
if (!menuIds?.length) return res.json({ code: 40017, msg: '请至少选择一个页面权限', data: null })
|
|
if (!DATA_SCOPES.includes(dataScope)) return res.json({ code: 40018, msg: '请选择数据权限范围', data: null })
|
|
const duplicate = roles.some((r) => r.orgId === orgId && r.name === name)
|
|
if (duplicate) return res.json({ code: 40019, msg: '同一所属机构下角色名称不可重复', data: null })
|
|
|
|
const role = {
|
|
id: nextRoleId(),
|
|
name,
|
|
orgId,
|
|
builtin: false,
|
|
status: 'enabled',
|
|
menuIds,
|
|
buttonCodes: buttonCodes || [],
|
|
dataScope,
|
|
createdAt: nowStr()
|
|
}
|
|
roles.push(role)
|
|
res.json({ code: 200, msg: 'ok', data: role })
|
|
})
|
|
|
|
router.put('/role/:id', (req, res) => {
|
|
const { id } = req.params
|
|
const { name, orgId, menuIds, buttonCodes, dataScope } = req.body || {}
|
|
const role = roles.find((r) => r.id === id)
|
|
if (!role) return res.json({ code: 40404, msg: '角色不存在', data: null })
|
|
if (!name) return res.json({ code: 40016, msg: '请输入角色名称', data: null })
|
|
if (role.builtin && name !== role.name) return res.json({ code: 40020, msg: '内置角色不可改名', data: null })
|
|
if (!orgId || !orgs.some((o) => o.id === orgId)) return res.json({ code: 40013, msg: '请选择有效的所属机构', data: null })
|
|
if (!menuIds?.length) return res.json({ code: 40017, msg: '请至少选择一个页面权限', data: null })
|
|
if (!DATA_SCOPES.includes(dataScope)) return res.json({ code: 40018, msg: '请选择数据权限范围', data: null })
|
|
const duplicate = roles.some((r) => r.id !== id && r.orgId === orgId && r.name === name)
|
|
if (duplicate) return res.json({ code: 40019, msg: '同一所属机构下角色名称不可重复', data: null })
|
|
|
|
role.name = name
|
|
role.orgId = orgId
|
|
role.menuIds = menuIds
|
|
role.buttonCodes = buttonCodes || []
|
|
role.dataScope = dataScope
|
|
res.json({ code: 200, msg: 'ok', data: role })
|
|
})
|
|
|
|
router.delete('/role/batch', (req, res) => {
|
|
const { ids } = req.body || {}
|
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
return res.json({ code: 40024, msg: '请选择要删除的角色', data: null })
|
|
}
|
|
const successIds = []
|
|
const failed = []
|
|
ids.forEach((id) => {
|
|
const role = roles.find((r) => r.id === id)
|
|
if (!role) {
|
|
failed.push({ id, name: id, reason: '角色不存在' })
|
|
return
|
|
}
|
|
if (role.builtin) {
|
|
failed.push({ id, name: role.name, reason: '内置角色不可删除' })
|
|
return
|
|
}
|
|
const relatedUsers = users.filter((u) => u.roleIds.includes(id))
|
|
if (relatedUsers.length) {
|
|
failed.push({ id, name: role.name, reason: `已关联${relatedUsers.length}个用户,请先解除关联` })
|
|
return
|
|
}
|
|
successIds.push(id)
|
|
})
|
|
successIds.forEach((id) => {
|
|
const index = roles.findIndex((r) => r.id === id)
|
|
if (index !== -1) roles.splice(index, 1)
|
|
})
|
|
res.json({ code: 200, msg: 'ok', data: { successIds, failed } })
|
|
})
|
|
|
|
// ---------------- 权限字典树 ----------------
|
|
|
|
router.get('/permission/tree', (req, res) => {
|
|
res.json({ code: 200, msg: 'ok', data: menuTree })
|
|
})
|
|
|
|
export default router
|
|
|
|
|