后台新增RBAC权限模型支持按角色配置接单管理权限
- 新增 admin_roles/admin_permissions/admin_role_permissions 三表与种子数据(059迁移) - 接单平台全部管理路由从固定角色切换为细粒度权限校验 - 撤单按动作二次校验:退回大厅与取消订单分别要求对应权限 - 管理员账号页新增角色权限配置卡片(admin专属,带审计日志) - 权限更新走事务保证原子性;session实时解析权限确保修改立即生效
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
-- 后台 RBAC 权限模型。保留 admin_users.role 兼容旧账号,权限通过角色键动态解析。
|
||||
CREATE TABLE IF NOT EXISTS admin_roles (
|
||||
role_key TEXT PRIMARY KEY,
|
||||
role_name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
is_system BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_permissions (
|
||||
permission_key TEXT PRIMARY KEY,
|
||||
permission_name TEXT NOT NULL,
|
||||
permission_group TEXT NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
is_system BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_role_permissions (
|
||||
role_key TEXT NOT NULL REFERENCES admin_roles(role_key) ON DELETE CASCADE,
|
||||
permission_key TEXT NOT NULL REFERENCES admin_permissions(permission_key) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (role_key, permission_key)
|
||||
);
|
||||
|
||||
INSERT INTO admin_roles (role_key, role_name, description)
|
||||
VALUES
|
||||
('admin', '管理员', '拥有全部后台权限'),
|
||||
('operator', '普通运营', '负责接单平台和订单运营'),
|
||||
('support', '客服', '负责客服处理和售后协助')
|
||||
ON CONFLICT (role_key) DO UPDATE SET
|
||||
role_name = EXCLUDED.role_name,
|
||||
description = EXCLUDED.description,
|
||||
updated_at = NOW();
|
||||
|
||||
INSERT INTO admin_permissions (permission_key, permission_name, permission_group, description)
|
||||
VALUES
|
||||
('worker_order.view', '查看接单订单', '接单管理', '查看接单平台订单及详情'),
|
||||
('worker_order.edit', '编辑接单订单', '接单管理', '修改接单订单基础资料'),
|
||||
('worker_order.configure_share', '配置拼单', '接单管理', '配置接单订单拼单规则'),
|
||||
('worker_order.assign', '指派打手', '接单管理', '指派或取消指派打手'),
|
||||
('worker_order.return_to_hall', '撤单并退回大厅', '接单管理', '将进行中的订单撤回抢单大厅'),
|
||||
('worker_order.cancel', '撤单并取消订单', '接单管理', '取消订单并终止后续履约'),
|
||||
('worker_order.cancel_share', '撤销拼单参与者', '接单管理', '撤销拼单中的单个参与者'),
|
||||
('worker_order.delete', '删除接单订单', '接单管理', '删除尚未履约的接单订单'),
|
||||
('worker_order.publish', '发布接单订单', '接单管理', '发布或下架接单订单'),
|
||||
('worker_order.accept', '验收接单订单', '接单管理', '验收接单订单和拼单份额'),
|
||||
('worker_order.reopen', '重新启用订单', '接单管理', '重新启用已取消的接单订单'),
|
||||
('worker_order.update_material', '更新订单资料', '接单管理', '补充或更新订单资料'),
|
||||
('worker_order.manage_problem', '处理问题单', '接单管理', '标记和处置问题单'),
|
||||
('worker_order.deduct_deposit', '扣除押金', '接单管理', '处理待解冻押金'),
|
||||
('worker_cancel_request.review', '审核撤单申请', '接单管理', '审核打手提交的撤单申请')
|
||||
ON CONFLICT (permission_key) DO UPDATE SET
|
||||
permission_name = EXCLUDED.permission_name,
|
||||
permission_group = EXCLUDED.permission_group,
|
||||
description = EXCLUDED.description,
|
||||
updated_at = NOW();
|
||||
|
||||
-- 管理员默认拥有全部已登记权限。
|
||||
INSERT INTO admin_role_permissions (role_key, permission_key)
|
||||
SELECT 'admin', permission_key FROM admin_permissions
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 普通运营保持现有完整接单操作;客服默认不拥有高风险直接撤单权限。
|
||||
INSERT INTO admin_role_permissions (role_key, permission_key)
|
||||
SELECT 'operator', permission_key
|
||||
FROM admin_permissions
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO admin_role_permissions (role_key, permission_key)
|
||||
SELECT 'support', permission_key
|
||||
FROM admin_permissions
|
||||
WHERE permission_key IN (
|
||||
'worker_order.view',
|
||||
'worker_order.edit',
|
||||
'worker_order.assign',
|
||||
'worker_order.publish',
|
||||
'worker_order.update_material',
|
||||
'worker_order.manage_problem',
|
||||
'worker_cancel_request.review'
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_admin_role_permissions_permission
|
||||
ON admin_role_permissions(permission_key, role_key);
|
||||
@@ -10,6 +10,7 @@ import kuaishouIndustryRouter from './admin/kuaishou-industry.js'
|
||||
import ordersRouter from './admin/orders.js'
|
||||
import notificationsRouter from './admin/notifications.js'
|
||||
import realtimeRouter from './admin/realtime.js'
|
||||
import rbacRouter from './admin/rbac.js'
|
||||
import platformConfigRouter from './admin/platform-config.js'
|
||||
import { requireAdminSession } from './admin/session.js'
|
||||
import tasksRouter from './admin/tasks.js'
|
||||
@@ -24,6 +25,7 @@ router.use(requireAdminSession)
|
||||
router.use(filesRouter)
|
||||
router.use(dashboardRouter)
|
||||
router.use(usersRouter)
|
||||
router.use(rbacRouter)
|
||||
router.use(auditLogsRouter)
|
||||
router.use(platformConfigRouter)
|
||||
router.use(kuaishouIndustryRouter)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import {
|
||||
listAdminRolesWithPermissions,
|
||||
updateAdminRolePermissions,
|
||||
} from '../../services/admin/admin-rbac-service.js'
|
||||
import { createJsonHandler, requireAdminRoles } from './session.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.use('/rbac', requireAdminRoles(['admin']))
|
||||
|
||||
router.get(
|
||||
'/rbac',
|
||||
createJsonHandler(() => listAdminRolesWithPermissions(), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取角色权限失败',
|
||||
scope: '[admin/rbac]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.put(
|
||||
'/rbac/roles/:roleKey/permissions',
|
||||
createJsonHandler(
|
||||
(req) => updateAdminRolePermissions(req.params.roleKey, req.body?.permissions),
|
||||
{
|
||||
successMessage: '角色权限已更新',
|
||||
errorMessage: '更新角色权限失败',
|
||||
scope: '[admin/rbac/roles/:roleKey/permissions]',
|
||||
audit: (req, data) => ({
|
||||
action: 'admin_role_permissions_updated',
|
||||
targetType: 'admin_role',
|
||||
targetId: String(req.params.roleKey || ''),
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
export default router
|
||||
@@ -80,6 +80,45 @@ export function requireAdminRoles(allowedRoles: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
export function requireAdminPermission(permission: string) {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
try {
|
||||
const session = getRequiredAdminSession(req)
|
||||
if (!session.permissions.includes(String(permission || '').trim())) {
|
||||
throw createHttpError('当前账号没有此操作权限', {
|
||||
statusCode: 403,
|
||||
errorCode: 'admin_permission_denied',
|
||||
})
|
||||
}
|
||||
next()
|
||||
} catch (error) {
|
||||
sendRouteError(res, error, '后台鉴权失败', '[admin/auth/permission]')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function requireAnyAdminPermission(permissions: string[]) {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
try {
|
||||
const session = getRequiredAdminSession(req)
|
||||
if (
|
||||
permissions.some((permission) =>
|
||||
session.permissions.includes(String(permission || '').trim()),
|
||||
)
|
||||
) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
throw createHttpError('当前账号没有此操作权限', {
|
||||
statusCode: 403,
|
||||
errorCode: 'admin_permission_denied',
|
||||
})
|
||||
} catch (error) {
|
||||
sendRouteError(res, error, '后台鉴权失败', '[admin/auth/permission]')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getRequiredAdminSession(req: Request): AdminSession {
|
||||
if (req.adminSession) {
|
||||
return req.adminSession
|
||||
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
markAdminWorkOrderProblem,
|
||||
resolveAdminProblemWorkOrder,
|
||||
} from '../../../services/worker-platform/index.js'
|
||||
import { createJsonHandler, requireAdminRoles } from '../session.js'
|
||||
import { createJsonHandler, requireAdminPermission } from '../session.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/problem',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.manage_problem'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
markAdminWorkOrderProblem(
|
||||
@@ -38,7 +38,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/problem/resolve',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.manage_problem'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
resolveAdminProblemWorkOrder(
|
||||
@@ -62,7 +62,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/accept',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminPermission('worker_order.accept'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
acceptAdminWorkOrder(String(req.params.workOrderId || ''), req.adminSession?.username || ''),
|
||||
@@ -82,7 +82,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/shares/:shareId/accept',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminPermission('worker_order.accept'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
acceptAdminWorkOrderShare(
|
||||
@@ -106,7 +106,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/shares/:shareId/cancel',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.cancel_share'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
cancelAdminWorkOrderShare(
|
||||
@@ -131,7 +131,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/deduct-pending-deposit',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminPermission('worker_order.deduct_deposit'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
deductAdminWorkOrderPendingDeposit(
|
||||
|
||||
@@ -25,13 +25,18 @@ import {
|
||||
updateAdminWorkOrder,
|
||||
updateAdminWorkOrderSharing,
|
||||
} from '../../../services/worker-platform/index.js'
|
||||
import { createJsonHandler, requireAdminRoles } from '../session.js'
|
||||
import {
|
||||
createJsonHandler,
|
||||
requireAdminPermission,
|
||||
requireAnyAdminPermission,
|
||||
requireAdminRoles,
|
||||
} from '../session.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get(
|
||||
'/worker-platform/orders',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.view'),
|
||||
createJsonHandler((req) => listAdminWorkOrders(req.query), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取接单订单失败',
|
||||
@@ -138,7 +143,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/material',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.update_material'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
submitAdminWorkOrderMaterial(
|
||||
@@ -162,7 +167,7 @@ router.post(
|
||||
|
||||
router.put(
|
||||
'/worker-platform/orders/:workOrderId',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.edit'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
updateAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}, {
|
||||
@@ -185,7 +190,7 @@ router.put(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/assign',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.assign'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
assignAdminWorkOrderToWorker(
|
||||
@@ -209,7 +214,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/unassign',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.assign'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
unassignAdminWorkOrder(
|
||||
@@ -232,13 +237,14 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/cancel',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAnyAdminPermission(['worker_order.return_to_hall', 'worker_order.cancel']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
cancelAdminWorkOrder(
|
||||
String(req.params.workOrderId || ''),
|
||||
req.body || {},
|
||||
req.adminSession?.username || '',
|
||||
req.adminSession?.permissions || [],
|
||||
),
|
||||
{
|
||||
successMessage: '撤单处理完成,打手额度已恢复',
|
||||
@@ -259,7 +265,7 @@ router.post(
|
||||
|
||||
router.delete(
|
||||
'/worker-platform/orders/:workOrderId',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminPermission('worker_order.delete'),
|
||||
createJsonHandler((req) => deleteAdminWorkOrder(String(req.params.workOrderId || '')), {
|
||||
successMessage: '工单已删除',
|
||||
errorMessage: '删除工单失败',
|
||||
@@ -275,7 +281,7 @@ router.delete(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/reopen',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminPermission('worker_order.reopen'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
reopenAdminCancelledWorkOrder(
|
||||
@@ -298,7 +304,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/publish',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.publish'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
publishAdminWorkOrder(String(req.params.workOrderId || ''), req.adminSession?.username || ''),
|
||||
@@ -318,7 +324,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/unpublish',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.publish'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
unpublishAdminWorkOrder(
|
||||
@@ -341,7 +347,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/pin',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.publish'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
pinAdminWorkOrder(
|
||||
@@ -365,7 +371,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/confirm-friend',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.manage_problem'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
confirmAdminWorkOrderFriendAdded(
|
||||
@@ -388,7 +394,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/reset-gift-material',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.manage_problem'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
resetAdminWorkOrderGiftMaterial(
|
||||
@@ -412,7 +418,7 @@ router.post(
|
||||
|
||||
router.get(
|
||||
'/worker-platform/orders/:workOrderId/sharing',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.view'),
|
||||
createJsonHandler((req) => getAdminWorkOrderSharing(String(req.params.workOrderId || '')), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取拼单信息失败',
|
||||
@@ -422,7 +428,7 @@ router.get(
|
||||
|
||||
router.get(
|
||||
'/worker-platform/orders/:workOrderId/events',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.view'),
|
||||
createJsonHandler((req) => getAdminWorkOrderEvents(String(req.params.workOrderId || '')), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取工单流转记录失败',
|
||||
@@ -432,7 +438,7 @@ router.get(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/sharing',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminPermission('worker_order.configure_share'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
updateAdminWorkOrderSharing(
|
||||
@@ -456,7 +462,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/accept-batch',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminPermission('worker_order.accept'),
|
||||
createJsonHandler(
|
||||
(req) => acceptAdminWorkOrders(req.body || {}, req.adminSession?.username || ''),
|
||||
{
|
||||
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
resolveAdminWorkerOrderFeedback,
|
||||
reviewAdminWorkerCancelRequest,
|
||||
} from '../../../services/worker-platform/index.js'
|
||||
import { createJsonHandler, requireAdminRoles } from '../session.js'
|
||||
import { createJsonHandler, requireAdminPermission } from '../session.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get(
|
||||
'/worker-platform/cancel-requests',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_cancel_request.review'),
|
||||
createJsonHandler((req) => listAdminWorkerCancelRequests(req.query), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取撤单申请失败',
|
||||
@@ -22,7 +22,7 @@ router.get(
|
||||
|
||||
router.get(
|
||||
'/worker-platform/feedbacks',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.manage_problem'),
|
||||
createJsonHandler((req) => listAdminWorkerOrderFeedbacks(req.query), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取问题反馈失败',
|
||||
@@ -32,7 +32,7 @@ router.get(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/feedbacks/:feedbackId/resolve',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_order.manage_problem'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
resolveAdminWorkerOrderFeedback(
|
||||
@@ -50,7 +50,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/worker-platform/cancel-requests/:requestId/review',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
requireAdminPermission('worker_cancel_request.review'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
reviewAdminWorkerCancelRequest(
|
||||
|
||||
@@ -20,6 +20,7 @@ import { addHours, nowIso } from '../../utils/time.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { normalizePage, normalizePageSize } from './admin-query-utils.js'
|
||||
import { recordAdminLoginLog } from './admin-login-log-service.js'
|
||||
import { listAdminPermissionKeysForRole } from './admin-rbac-service.js'
|
||||
|
||||
type AdminUserRow = NonNullable<Awaited<ReturnType<typeof getAdminUserById>>>
|
||||
|
||||
@@ -30,6 +31,7 @@ export type AdminSession = {
|
||||
role: AdminRole
|
||||
expiresAt: string
|
||||
sessionVersion: number
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
type AdminRole = 'admin' | 'operator' | 'support'
|
||||
@@ -230,6 +232,7 @@ export async function verifyAdminSessionToken(token: unknown): Promise<AdminSess
|
||||
role: normalizeAdminRole(user.role),
|
||||
expiresAt,
|
||||
sessionVersion: currentSessionVersion,
|
||||
permissions: await listAdminPermissionKeysForRole(normalizeAdminRole(user.role)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +252,7 @@ export async function getAdminSessionSummary(token: unknown): Promise<JsonObject
|
||||
userId: session.userId,
|
||||
username: session.username,
|
||||
role: session.role,
|
||||
permissions: session.permissions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
|
||||
export const ADMIN_PERMISSION_KEYS = [
|
||||
'worker_order.view',
|
||||
'worker_order.edit',
|
||||
'worker_order.configure_share',
|
||||
'worker_order.assign',
|
||||
'worker_order.return_to_hall',
|
||||
'worker_order.cancel',
|
||||
'worker_order.cancel_share',
|
||||
'worker_order.delete',
|
||||
'worker_order.publish',
|
||||
'worker_order.accept',
|
||||
'worker_order.reopen',
|
||||
'worker_order.update_material',
|
||||
'worker_order.manage_problem',
|
||||
'worker_order.deduct_deposit',
|
||||
'worker_cancel_request.review',
|
||||
] as const
|
||||
|
||||
export type AdminPermissionKey = (typeof ADMIN_PERMISSION_KEYS)[number]
|
||||
|
||||
type PermissionRow = {
|
||||
permission_key: string
|
||||
permission_name: string
|
||||
permission_group: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export async function listAdminPermissionKeysForRole(role: string): Promise<string[]> {
|
||||
const result = await query<{ permission_key: string }>(
|
||||
`
|
||||
SELECT arp.permission_key
|
||||
FROM admin_role_permissions arp
|
||||
WHERE arp.role_key = $1
|
||||
ORDER BY arp.permission_key
|
||||
`,
|
||||
[
|
||||
String(role || '')
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
],
|
||||
)
|
||||
return result.rows.map((row) => String(row.permission_key || '').trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
export async function listAdminRolesWithPermissions() {
|
||||
const result = await query<
|
||||
PermissionRow & { role_key: string; role_name: string; role_description: string }
|
||||
>(
|
||||
`
|
||||
SELECT
|
||||
ar.role_key,
|
||||
ar.role_name,
|
||||
ar.description AS role_description,
|
||||
ap.permission_key,
|
||||
ap.permission_name,
|
||||
ap.permission_group,
|
||||
ap.description
|
||||
FROM admin_roles ar
|
||||
CROSS JOIN admin_permissions ap
|
||||
ORDER BY ar.role_key, ap.permission_group, ap.permission_key
|
||||
`,
|
||||
)
|
||||
|
||||
const roleMap = new Map<
|
||||
string,
|
||||
{
|
||||
roleKey: string
|
||||
roleName: string
|
||||
description: string
|
||||
permissions: Array<{
|
||||
permissionKey: string
|
||||
permissionName: string
|
||||
permissionGroup: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
}>
|
||||
}
|
||||
>()
|
||||
|
||||
const enabled = await query<{ role_key: string; permission_key: string }>(
|
||||
'SELECT role_key, permission_key FROM admin_role_permissions',
|
||||
)
|
||||
const enabledSet = new Set(enabled.rows.map((row) => `${row.role_key}:${row.permission_key}`))
|
||||
|
||||
for (const row of result.rows) {
|
||||
let role = roleMap.get(row.role_key)
|
||||
if (!role) {
|
||||
role = {
|
||||
roleKey: row.role_key,
|
||||
roleName: row.role_name,
|
||||
description: row.role_description,
|
||||
permissions: [],
|
||||
}
|
||||
roleMap.set(row.role_key, role)
|
||||
}
|
||||
role.permissions.push({
|
||||
permissionKey: row.permission_key,
|
||||
permissionName: row.permission_name,
|
||||
permissionGroup: row.permission_group,
|
||||
description: row.description,
|
||||
enabled: enabledSet.has(`${row.role_key}:${row.permission_key}`),
|
||||
})
|
||||
}
|
||||
|
||||
return { roles: Array.from(roleMap.values()) }
|
||||
}
|
||||
|
||||
export async function updateAdminRolePermissions(roleKey: unknown, permissionKeys: unknown) {
|
||||
const normalizedRole = String(roleKey || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (!['admin', 'operator', 'support'].includes(normalizedRole)) {
|
||||
throw createHttpError('角色不存在或不可修改', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_role_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
if (!Array.isArray(permissionKeys)) {
|
||||
throw createHttpError('权限列表格式无效', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_permissions_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const requested = Array.from(
|
||||
new Set(
|
||||
permissionKeys
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter((item): item is AdminPermissionKey =>
|
||||
(ADMIN_PERMISSION_KEYS as readonly string[]).includes(item),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if (normalizedRole === 'admin' && requested.length !== ADMIN_PERMISSION_KEYS.length) {
|
||||
throw createHttpError('管理员角色必须保留全部权限', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_role_full_permissions_required',
|
||||
})
|
||||
}
|
||||
|
||||
await withTransaction(async (client) => {
|
||||
await client.query('DELETE FROM admin_role_permissions WHERE role_key = $1', [normalizedRole])
|
||||
if (requested.length > 0) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO admin_role_permissions (role_key, permission_key)
|
||||
SELECT $1, permission_key
|
||||
FROM admin_permissions
|
||||
WHERE permission_key = ANY($2::text[])
|
||||
ON CONFLICT DO NOTHING
|
||||
`,
|
||||
[normalizedRole, requested],
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return (await listAdminRolesWithPermissions()).roles.find(
|
||||
(role) => role.roleKey === normalizedRole,
|
||||
)
|
||||
}
|
||||
@@ -55,6 +55,7 @@ export async function cancelAdminWorkOrder(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
actorPermissions: string[] = [],
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS || !workOrder.assigned_worker_id) {
|
||||
@@ -70,6 +71,14 @@ export async function cancelAdminWorkOrder(
|
||||
errorCode: 'work_order_cancel_action_required',
|
||||
})
|
||||
}
|
||||
const requiredPermission =
|
||||
action === 'return_to_hall' ? 'worker_order.return_to_hall' : 'worker_order.cancel'
|
||||
if (!actorPermissions.includes(requiredPermission)) {
|
||||
throw createHttpError('当前账号没有此撤单权限', {
|
||||
statusCode: 403,
|
||||
errorCode: 'admin_permission_denied',
|
||||
})
|
||||
}
|
||||
const reason = String(payload.reason || '').trim()
|
||||
if (action === 'cancel_order' && !reason) {
|
||||
throw createHttpError('取消订单时请填写退款或撤单原因', {
|
||||
|
||||
Reference in New Issue
Block a user