后台新增RBAC权限模型支持按角色配置接单管理权限

- 新增 admin_roles/admin_permissions/admin_role_permissions 三表与种子数据(059迁移)
- 接单平台全部管理路由从固定角色切换为细粒度权限校验
- 撤单按动作二次校验:退回大厅与取消订单分别要求对应权限
- 管理员账号页新增角色权限配置卡片(admin专属,带审计日志)
- 权限更新走事务保证原子性;session实时解析权限确保修改立即生效
This commit is contained in:
yml2213
2026-08-23 16:04:10 +08:00
parent fb0e9c8f9c
commit 3df271b30c
16 changed files with 610 additions and 70 deletions
@@ -53,6 +53,7 @@ import {
deductAdminWorkOrderPendingDeposit,
deleteAdminWorkOrder,
fetchAdminWorkCategories,
fetchAdminSession,
fetchAdminWorkOrderSharing,
fetchAdminWorkerLevels,
fetchAdminWorkerUsers,
@@ -72,7 +73,7 @@ import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-pla
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
import { hasAdminRole } from '@/utils/admin-auth'
import { hasAdminPermission, hasAdminRole } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
import {
DeadlineCountdown,
@@ -151,6 +152,51 @@ export default function WorkOrdersPanel() {
const queryClient = useQueryClient()
const canEditRequirementFields = hasAdminRole('operator')
const canViewStatistics = hasAdminRole('operator')
const adminSessionQuery = useQuery({
queryKey: ['admin-session'],
queryFn: fetchAdminSession,
retry: false,
})
const sessionPermissions = adminSessionQuery.data?.data.user.permissions
const canReturnToHall = sessionPermissions
? sessionPermissions.includes('worker_order.return_to_hall')
: hasAdminPermission('worker_order.return_to_hall')
const canCancelOrder = sessionPermissions
? sessionPermissions.includes('worker_order.cancel')
: hasAdminPermission('worker_order.cancel')
const canCancelShare = sessionPermissions
? sessionPermissions.includes('worker_order.cancel_share')
: hasAdminPermission('worker_order.cancel_share')
const canEditOrder = sessionPermissions
? sessionPermissions.includes('worker_order.edit')
: hasAdminPermission('worker_order.edit')
const canAssignOrder = sessionPermissions
? sessionPermissions.includes('worker_order.assign')
: hasAdminPermission('worker_order.assign')
const canDeleteOrder = sessionPermissions
? sessionPermissions.includes('worker_order.delete')
: hasAdminPermission('worker_order.delete')
const canReopenOrder = sessionPermissions
? sessionPermissions.includes('worker_order.reopen')
: hasAdminPermission('worker_order.reopen')
const canPublishOrder = sessionPermissions
? sessionPermissions.includes('worker_order.publish')
: hasAdminPermission('worker_order.publish')
const canAcceptOrder = sessionPermissions
? sessionPermissions.includes('worker_order.accept')
: hasAdminPermission('worker_order.accept')
const canUpdateMaterial = sessionPermissions
? sessionPermissions.includes('worker_order.update_material')
: hasAdminPermission('worker_order.update_material')
const canManageProblem = sessionPermissions
? sessionPermissions.includes('worker_order.manage_problem')
: hasAdminPermission('worker_order.manage_problem')
const canDeductDeposit = sessionPermissions
? sessionPermissions.includes('worker_order.deduct_deposit')
: hasAdminPermission('worker_order.deduct_deposit')
const canConfigureSharing = sessionPermissions
? sessionPermissions.includes('worker_order.configure_share')
: hasAdminPermission('worker_order.configure_share')
const [statuses, setStatuses] = useState<string[]>([])
const [targetWorkOrderId, setTargetWorkOrderId] = useState<number | undefined>()
const [keyword, setKeyword] = useState('')
@@ -976,17 +1022,17 @@ export default function WorkOrdersPanel() {
render: (_, row) => (
<Space wrap>
<Button onClick={() => openDetailDrawer(row)}></Button>
{row.capabilities?.canEdit ? (
{row.capabilities?.canEdit && canEditOrder ? (
<Button icon={<EditOutlined />} onClick={() => openEditModal(row)}>
</Button>
) : null}
{row.capabilities?.canDelete ? (
{row.capabilities?.canDelete && canDeleteOrder ? (
<Button danger icon={<DeleteOutlined />} onClick={() => requestDeleteWorkOrder(row)}>
</Button>
) : null}
{row.capabilities?.canReopen ? (
{row.capabilities?.canReopen && canReopenOrder ? (
<Button
icon={<UndoOutlined />}
onClick={() => {
@@ -1006,29 +1052,30 @@ export default function WorkOrdersPanel() {
</Button>
) : null}
{row.sharing?.enabled || row.capabilities?.canConfigureSharing ? (
{row.sharing?.enabled ||
(row.capabilities?.canConfigureSharing && canConfigureSharing) ? (
<Button icon={<TeamOutlined />} onClick={() => openSharingConfigModal(row)}>
{row.sharing?.enabled && !row.capabilities?.canConfigureSharing
? '拼单明细'
: '拼单配置'}
</Button>
) : null}
{row.capabilities?.canAssign ? (
{row.capabilities?.canAssign && canAssignOrder ? (
<Button icon={<UserAddOutlined />} onClick={() => openAssignModal(row)}>
</Button>
) : null}
{row.capabilities?.canCancel ? (
{row.capabilities?.canCancel && (canReturnToHall || canCancelOrder) ? (
<Button danger icon={<UndoOutlined />} onClick={() => openCancelModal(row)}>
</Button>
) : null}
{row.capabilities?.canUpdateMaterial ? (
{row.capabilities?.canUpdateMaterial && canUpdateMaterial ? (
<Button icon={<FormOutlined />} onClick={() => openMaterialModal(row)}>
{['open', 'in_progress'].includes(row.status) ? '更新资料' : '补资料'}
</Button>
) : null}
{row.capabilities?.canPublish ? (
{row.capabilities?.canPublish && canPublishOrder ? (
<Button
icon={<SendOutlined />}
disabled={Number(row.rewardAmount || 0) <= 0}
@@ -1040,7 +1087,7 @@ export default function WorkOrdersPanel() {
</Button>
) : null}
{row.capabilities?.canPin ? (
{row.capabilities?.canPin && canPublishOrder ? (
<Button
icon={<PushpinOutlined />}
title={row.pinnedAt ? '取消置顶' : '置顶后抢单大厅优先展示'}
@@ -1054,7 +1101,7 @@ export default function WorkOrdersPanel() {
{row.pinnedAt ? '取消置顶' : '置顶'}
</Button>
) : null}
{row.capabilities?.canUnpublish ? (
{row.capabilities?.canUnpublish && canPublishOrder ? (
<Button
danger
onClick={() => {
@@ -1071,7 +1118,7 @@ export default function WorkOrdersPanel() {
</Button>
) : null}
{row.capabilities?.canAccept ? (
{row.capabilities?.canAccept && canAcceptOrder ? (
<Button
icon={<CheckOutlined />}
type="primary"
@@ -1080,7 +1127,9 @@ export default function WorkOrdersPanel() {
</Button>
) : null}
{row.capabilities?.canDeduct && Number(row.pendingUnfreezeAmount || 0) > 0 ? (
{row.capabilities?.canDeduct &&
canDeductDeposit &&
Number(row.pendingUnfreezeAmount || 0) > 0 ? (
<Button icon={<WarningOutlined />} danger onClick={() => openDeductModal(row)}>
</Button>
@@ -1098,7 +1147,7 @@ export default function WorkOrdersPanel() {
</Button>
) : null}
{row.capabilities?.canConfirmFriend ? (
{row.capabilities?.canConfirmFriend && canManageProblem ? (
<Button
type="primary"
icon={<UserAddOutlined />}
@@ -1107,17 +1156,17 @@ export default function WorkOrdersPanel() {
</Button>
) : null}
{row.capabilities?.canResetGiftFlow ? (
{row.capabilities?.canResetGiftFlow && canManageProblem ? (
<Button danger onClick={() => requestResetGiftMaterial(row)}>
</Button>
) : null}
{row.capabilities?.canMarkProblem ? (
{row.capabilities?.canMarkProblem && canManageProblem ? (
<Button icon={<WarningOutlined />} danger onClick={() => setProblemOrder(row)}>
</Button>
) : null}
{row.capabilities?.canResolveProblem ? (
{row.capabilities?.canResolveProblem && canManageProblem ? (
<Button icon={<FormOutlined />} onClick={() => setResolutionOrder(row)}>
</Button>
@@ -1470,7 +1519,7 @@ export default function WorkOrdersPanel() {
<Tag color={resolveGiftPhaseColor(detailOrder.gift.phase)}>
{formatGiftPhase(detailOrder.gift.phase)}
</Tag>
{detailOrder.capabilities?.canConfirmFriend ? (
{detailOrder.capabilities?.canConfirmFriend && canManageProblem ? (
<Button
type="primary"
size="small"
@@ -1479,7 +1528,7 @@ export default function WorkOrdersPanel() {
</Button>
) : null}
{detailOrder.capabilities?.canResetGiftFlow ? (
{detailOrder.capabilities?.canResetGiftFlow && canManageProblem ? (
<Button
size="small"
danger
@@ -1814,7 +1863,7 @@ export default function WorkOrdersPanel() {
<Form
form={cancelForm}
layout="vertical"
initialValues={{ action: 'return_to_hall' }}
initialValues={{ action: canReturnToHall ? 'return_to_hall' : 'cancel_order' }}
onFinish={submitCancelOrder}
>
<Alert
@@ -1830,8 +1879,12 @@ export default function WorkOrdersPanel() {
>
<Radio.Group>
<Space direction="vertical">
<Radio value="return_to_hall">退</Radio>
<Radio value="cancel_order">退</Radio>
{canReturnToHall ? (
<Radio value="return_to_hall">退</Radio>
) : null}
{canCancelOrder ? (
<Radio value="cancel_order">退</Radio>
) : null}
</Space>
</Radio.Group>
</Form.Item>
@@ -1988,7 +2041,7 @@ export default function WorkOrdersPanel() {
</Modal>
<Modal
title={`${sharingOrder?.capabilities?.canConfigureSharing ? '拼单配置' : '拼单明细'} · ${sharingOrder?.productName || ''}`}
title={`${sharingOrder?.capabilities?.canConfigureSharing && canConfigureSharing ? '拼单配置' : '拼单明细'} · ${sharingOrder?.productName || ''}`}
open={Boolean(sharingOrder)}
onCancel={() => {
setSharingOrder(null)
@@ -1997,7 +2050,9 @@ export default function WorkOrdersPanel() {
cancelShareForm.resetFields()
}}
onOk={() => sharingForm.submit()}
footer={sharingOrder?.capabilities?.canConfigureSharing ? undefined : null}
footer={
sharingOrder?.capabilities?.canConfigureSharing && canConfigureSharing ? undefined : null
}
destroyOnHidden
width={720}
>
@@ -2012,7 +2067,7 @@ export default function WorkOrdersPanel() {
unitReward: 0,
}}
onFinish={submitSharingConfig}
disabled={!sharingOrder.capabilities?.canConfigureSharing}
disabled={!sharingOrder.capabilities?.canConfigureSharing || !canConfigureSharing}
>
<Form.Item
label="启用拼单"
@@ -2169,17 +2224,19 @@ export default function WorkOrdersPanel() {
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
<Button
danger
size="small"
icon={<UndoOutlined />}
onClick={() => {
setCancelShare(share)
cancelShareForm.setFieldsValue({ reason: '' })
}}
>
</Button>
{canCancelShare ? (
<Button
danger
size="small"
icon={<UndoOutlined />}
onClick={() => {
setCancelShare(share)
cancelShareForm.setFieldsValue({ reason: '' })
}}
>
</Button>
) : null}
</Space>
)
},