diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 9bf1098..0c031ee 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -591,14 +591,14 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { adminRoutes.PUT("/chats/listing-group-welcome", requirePerm("system_config:update"), chatHandler.AdminUpdateListingGroupWelcome) // 二维码池管理 - adminRoutes.POST("/chats/qrcodes", requirePerm("chat:manage"), chatHandler.CreateQrCodeHandler) - adminRoutes.POST("/chats/qrcodes/batch", requirePerm("chat:manage"), chatHandler.BatchCreateQrCodeHandler) - adminRoutes.POST("/chats/qrcodes/batch-delete", requirePerm("chat:manage"), chatHandler.BatchDeleteQrCodeHandler) - adminRoutes.GET("/chats/qrcodes", requirePerm("chat:view"), chatHandler.ListQrCodesHandler) - adminRoutes.GET("/chats/qrcodes/stats", requirePerm("chat:view"), chatHandler.GetQrCodeStatsHandler) - adminRoutes.POST("/chats/qrcodes/ocr-group-name", requirePerm("chat:manage"), chatHandler.RecognizeQrCodeGroupNameHandler) - adminRoutes.PATCH("/chats/qrcodes/:id", requirePerm("chat:manage"), chatHandler.UpdateQrCodeHandler) - adminRoutes.DELETE("/chats/qrcodes/:id", requirePerm("chat:manage"), chatHandler.DeleteQrCodeHandler) + adminRoutes.POST("/chats/qrcodes", requirePerm("qrcode:manage"), chatHandler.CreateQrCodeHandler) + adminRoutes.POST("/chats/qrcodes/batch", requirePerm("qrcode:manage"), chatHandler.BatchCreateQrCodeHandler) + adminRoutes.POST("/chats/qrcodes/batch-delete", requirePerm("qrcode:manage"), chatHandler.BatchDeleteQrCodeHandler) + adminRoutes.GET("/chats/qrcodes", requirePerm("qrcode:view"), chatHandler.ListQrCodesHandler) + adminRoutes.GET("/chats/qrcodes/stats", requirePerm("qrcode:view"), chatHandler.GetQrCodeStatsHandler) + adminRoutes.POST("/chats/qrcodes/ocr-group-name", requirePerm("qrcode:manage"), chatHandler.RecognizeQrCodeGroupNameHandler) + adminRoutes.PATCH("/chats/qrcodes/:id", requirePerm("qrcode:manage"), chatHandler.UpdateQrCodeHandler) + adminRoutes.DELETE("/chats/qrcodes/:id", requirePerm("qrcode:manage"), chatHandler.DeleteQrCodeHandler) // 客服分组 adminRoutes.GET("/chat-support-groups", requirePerm("chat:manage"), supportGroupHandler.List) diff --git a/backend/migrations/000016_qrcode_permissions.sql b/backend/migrations/000016_qrcode_permissions.sql new file mode 100644 index 0000000..a7ae759 --- /dev/null +++ b/backend/migrations/000016_qrcode_permissions.sql @@ -0,0 +1,51 @@ +-- +goose Up +-- +goose StatementBegin + +INSERT INTO permissions (code, name, resource, action) VALUES +('qrcode:view', '查看二维码池', 'qrcode', 'view'), +('qrcode:manage', '管理二维码池', 'qrcode', 'manage') +ON DUPLICATE KEY UPDATE + name = VALUES(name), + resource = VALUES(resource), + action = VALUES(action); + +-- 超级管理员补齐新增权限。 +INSERT IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id +FROM roles r +JOIN permissions p ON p.code IN ('qrcode:view', 'qrcode:manage') +WHERE r.code = 'super_admin'; + +-- 客服默认可以查看和维护二维码池。 +INSERT IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id +FROM roles r +JOIN permissions p ON p.code IN ('qrcode:view', 'qrcode:manage') +WHERE r.code = 'cs'; + +-- 已拥有客服配置管理权限的角色,迁移后继续拥有二维码池管理能力。 +INSERT IGNORE INTO role_permissions (role_id, permission_id) +SELECT DISTINCT rp.role_id, p.id +FROM role_permissions rp +JOIN permissions oldp ON oldp.id = rp.permission_id AND oldp.code = 'chat:manage' +JOIN permissions p ON p.code IN ('qrcode:view', 'qrcode:manage'); + +-- 已拥有客服群聊查看权限的角色,迁移后继续能进入二维码池查看页。 +INSERT IGNORE INTO role_permissions (role_id, permission_id) +SELECT DISTINCT rp.role_id, p.id +FROM role_permissions rp +JOIN permissions oldp ON oldp.id = rp.permission_id AND oldp.code = 'chat:view' +JOIN permissions p ON p.code = 'qrcode:view'; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +DELETE rp FROM role_permissions rp +JOIN permissions p ON p.id = rp.permission_id +WHERE p.code IN ('qrcode:view', 'qrcode:manage'); + +DELETE FROM permissions WHERE code IN ('qrcode:view', 'qrcode:manage'); + +-- +goose StatementEnd diff --git a/frontend/src/features/admin/components/AssignPermissionsDialog.vue b/frontend/src/features/admin/components/AssignPermissionsDialog.vue index 686d7fc..b8726f9 100644 --- a/frontend/src/features/admin/components/AssignPermissionsDialog.vue +++ b/frontend/src/features/admin/components/AssignPermissionsDialog.vue @@ -102,6 +102,7 @@ const resourceLabels: Record = { listing: '商品管理', dispute: '仲裁中心', chat: '客服群聊', + qrcode: '二维码池', wallet: '资金流水', withdrawal: '提现审核', payment_config: '支付配置', diff --git a/frontend/src/features/admin/views/AdminQrCodePoolView.vue b/frontend/src/features/admin/views/AdminQrCodePoolView.vue index d8c4037..5ad9416 100644 --- a/frontend/src/features/admin/views/AdminQrCodePoolView.vue +++ b/frontend/src/features/admin/views/AdminQrCodePoolView.vue @@ -29,10 +29,13 @@ import { formatDateTime } from '@/shared/utils/time' import AdminTablePagination from '../components/AdminTablePagination.vue' import AuthImage from '@/shared/components/business/AuthImage.vue' import { readError } from '@/shared/utils/error' +import { useAdminSessionStore } from '@/stores/adminSession' +const adminSession = useAdminSessionStore() const loading = ref(false) const qrcodes = ref([]) const stats = ref({ unused_count: 0, used_count: 0, disabled_count: 0, total_count: 0 }) +const canManageQrCodes = computed(() => adminSession.hasPermission('qrcode:manage')) const currentPage = ref(1) const currentPageSize = ref(20) @@ -239,6 +242,7 @@ function handleUploadExceed() { } function openUpload() { + if (!canManageQrCodes.value) return clearUploadedImages() uploadForm.note = '' uploadForm.expires_at = '' @@ -246,6 +250,7 @@ function openUpload() { } async function submitUpload() { + if (!canManageQrCodes.value) return if (uploadPendingCount.value > 0) { ElMessage.warning('图片还在上传中,请稍后保存') return @@ -279,6 +284,7 @@ onBeforeUnmount(() => { }) function openEdit(row: ChatQrCode) { + if (!canManageQrCodes.value) return editForm.id = row.id editForm.image_url = row.image_url editForm.group_name = row.group_name || '' @@ -295,6 +301,7 @@ function clearEditExpiresAt() { } async function handleEditImageUpload(options: { file: File }) { + if (!canManageQrCodes.value) return editUploading.value = true try { const uploaded = await uploadAdminFile(options.file, 'qrcode') @@ -308,6 +315,7 @@ async function handleEditImageUpload(options: { file: File }) { } async function submitEdit() { + if (!canManageQrCodes.value) return await updateQrCode(editForm.id, { image_url: editForm.image_url, group_name: editForm.group_name, @@ -356,6 +364,7 @@ function setRenameLoading(id: number, loading: boolean) { } async function handleRenameFlagChange(row: ChatQrCode, value: string | number | boolean) { + if (!canManageQrCodes.value) return const nextValue = Boolean(value) const previous = !nextValue row.wecom_renamed = nextValue @@ -372,6 +381,7 @@ async function handleRenameFlagChange(row: ChatQrCode, value: string | number | } async function handleDisable(row: ChatQrCode) { + if (!canManageQrCodes.value) return await ElMessageBox.confirm('确定停用该二维码?停用后不再发放给新发布群。', '停用确认', { type: 'warning', }) @@ -381,10 +391,10 @@ async function handleDisable(row: ChatQrCode) { } async function handleDelete(row: ChatQrCode) { - const tip = - qrcodeWasIssued(row) - ? '确定删除这条已发放二维码记录?删除后仅清理二维码池旧记录,不会影响已发送到群聊里的图片消息。' - : '确定删除该二维码?删除后将从二维码池移除。' + if (!canManageQrCodes.value) return + const tip = qrcodeWasIssued(row) + ? '确定删除这条已发放二维码记录?删除后仅清理二维码池旧记录,不会影响已发送到群聊里的图片消息。' + : '确定删除该二维码?删除后将从二维码池移除。' await ElMessageBox.confirm(tip, '删除确认', { type: 'warning', }) @@ -394,6 +404,7 @@ async function handleDelete(row: ChatQrCode) { } async function handleBatchDelete() { + if (!canManageQrCodes.value) return if (!selectedCount.value) { ElMessage.warning('请先选择要删除的二维码') return @@ -460,6 +471,7 @@ onMounted(reloadAll)
已选 {{ selectedCount }} 条 刷新 - 添加二维码 + + 添加二维码 +
@@ -483,7 +497,12 @@ onMounted(reloadAll) row-key="id" @selection-change="handleSelectionChange" > - + - + @@ -594,7 +613,12 @@ onMounted(reloadAll) - +
diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue index 5c56911..1fc7f0d 100644 --- a/frontend/src/layouts/AdminLayout.vue +++ b/frontend/src/layouts/AdminLayout.vue @@ -104,7 +104,7 @@ const allNavGroups: NavGroup[] = [ permission: 'dispute:view', }, { label: '客服群聊', to: adminPath('chats'), icon: ChatDotRound, permission: 'chat:view' }, - { label: '二维码池', to: adminPath('qrcode-pool'), icon: Picture, permission: 'chat:view' }, + { label: '二维码池', to: adminPath('qrcode-pool'), icon: Picture, permission: 'qrcode:view' }, { label: '客服分组', to: adminPath('support-groups'), icon: User, permission: 'chat:manage' }, ], },