优化客服权限
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"sources": [
|
||||
{
|
||||
"key": "default",
|
||||
"label": "默认账号",
|
||||
"enabled": true,
|
||||
"baseUrl": "https://example.com",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"phone": "",
|
||||
"deviceId": "08bc9d8c-fd15-48ea-bc00-8d754076cafc",
|
||||
"deviceType": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"channels": {
|
||||
"bark": {
|
||||
"enabled": true,
|
||||
"serverUrl": "https://api.day.app",
|
||||
"recipients": []
|
||||
},
|
||||
"wpush": {
|
||||
"enabled": true,
|
||||
"recipients": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"jobs": [
|
||||
{
|
||||
"id": "cloudtentacles-health",
|
||||
"type": "cloudtentacles_health",
|
||||
"enabled": false,
|
||||
"intervalSeconds": 300,
|
||||
"cooldownSeconds": 1800,
|
||||
"config": {
|
||||
"assetThreshold": 500,
|
||||
"accounts": [
|
||||
{
|
||||
"sourceKey": "default",
|
||||
"label": "默认账号",
|
||||
"enabled": true,
|
||||
"assetThreshold": 500
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -81,7 +81,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry/vouchers/check-available',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
checkAdminKuaishouIndustryVoucherAvailable(
|
||||
@@ -98,7 +98,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry/vouchers/reverse',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
reverseAdminKuaishouIndustryVoucher(req.body as AdminKuaishouIndustryVoucherReverseRouteBody),
|
||||
@@ -113,7 +113,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry/vouchers/consume',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
consumeAdminKuaishouIndustryVoucherByCode(
|
||||
@@ -130,7 +130,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry/vouchers/resend-code',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
resendAdminKuaishouIndustryVoucherCode(
|
||||
|
||||
@@ -34,7 +34,7 @@ router.get('/orders/:orderId', createJsonHandler(
|
||||
|
||||
router.post(
|
||||
'/orders/:orderId/kuaishou-industry/resend-code',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
resendAdminOrderKuaishouIndustryVoucherCodes(
|
||||
|
||||
@@ -10,9 +10,9 @@ import fulfillmentRoutingRouter from "./platform-config/fulfillment-routing.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use("/platform-config", kuaishouIndustryRouter);
|
||||
router.use("/platform-config", requireAdminRoles(["admin"]));
|
||||
router.use("/platform-config", notificationsRouter);
|
||||
router.use("/platform-config", kuaishouIndustryRouter);
|
||||
router.use("/platform-config", kuaishouFeifeiRouter);
|
||||
router.use("/platform-config", ninetyoneRouter);
|
||||
router.use("/platform-config", fulfillmentRoutingRouter);
|
||||
|
||||
@@ -10,13 +10,14 @@ import type {
|
||||
AdminKuaishouIndustryAuthorizationCodeRouteBody,
|
||||
AdminKuaishouIndustrySourceConfigRouteBody,
|
||||
} from '../../../types/admin/route-inputs.js'
|
||||
import { createJsonHandler } from '../session.js'
|
||||
import { createJsonHandler, requireAdminRoles } from '../session.js'
|
||||
import type { JsonRecord } from '../../../types/json.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get(
|
||||
'/kuaishou-industry-source',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(() => getAdminKuaishouIndustrySourceConfig(), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取快手行业电子凭证配置失败',
|
||||
@@ -26,6 +27,7 @@ router.get(
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry-source',
|
||||
requireAdminRoles(['admin']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
updateAdminKuaishouIndustrySourceConfig(
|
||||
@@ -57,6 +59,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry-source/refresh-token',
|
||||
requireAdminRoles(['admin']),
|
||||
createJsonHandler((req) => refreshAdminKuaishouIndustryAccessToken(req.body as JsonRecord), {
|
||||
successMessage: '快手行业电子凭证 accessToken 已刷新',
|
||||
errorMessage: '刷新快手行业电子凭证 accessToken 失败',
|
||||
@@ -80,6 +83,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry-source/exchange-code',
|
||||
requireAdminRoles(['admin']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
exchangeAdminKuaishouIndustryAuthorizationCode(
|
||||
|
||||
@@ -149,7 +149,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/tasks/:taskId/kuaishou-industry/resend-code',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
resendAdminTaskKuaishouIndustryVoucherCode(getTaskId(req), req.adminSession || null),
|
||||
@@ -164,7 +164,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/tasks/:taskId/kuaishou-industry/consume',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
consumeAdminTaskKuaishouIndustryVoucher(
|
||||
|
||||
@@ -154,7 +154,7 @@ export async function getAdminOrderDetail(
|
||||
kuaishouIndustryVouchers: kuaishouIndustryVouchers.map(mapAdminKuaishouIndustryVoucher),
|
||||
operations: {
|
||||
canResendKuaishouIndustryVoucherCode:
|
||||
viewerContext.canManageTaskLifecycle && kuaishouIndustryVouchers.length > 0,
|
||||
viewerContext.canOperateKuaishouIndustryVoucher && kuaishouIndustryVouchers.length > 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -360,9 +360,9 @@ export async function getAdminTaskDetail(
|
||||
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
|
||||
canReturnKuaishouCloudFulfillmentStatus(task.task_status),
|
||||
canResendKuaishouIndustryVoucherCode:
|
||||
viewerContext.canManageTaskLifecycle && hasKuaishouIndustryVoucher,
|
||||
viewerContext.canOperateKuaishouIndustryVoucher && hasKuaishouIndustryVoucher,
|
||||
canConsumeKuaishouIndustryVoucher:
|
||||
viewerContext.canManageTaskLifecycle &&
|
||||
viewerContext.canOperateKuaishouIndustryVoucher &&
|
||||
hasKuaishouIndustryVoucher &&
|
||||
kuaishouIndustryVoucherStatus !== 'CONSUMED' &&
|
||||
kuaishouIndustryVoucherSendCallbackStatus === 'success',
|
||||
|
||||
@@ -39,6 +39,13 @@ test('canViewerCloseTask allows support to close active task but not redeemed ta
|
||||
assert.equal(canViewerCloseTask({ task_status: 'redeemed' }, viewerContext), false)
|
||||
})
|
||||
|
||||
test('createAdminViewerContext lets support operate vouchers without lifecycle permissions', () => {
|
||||
const viewerContext = createAdminViewerContext({ role: 'support' })
|
||||
|
||||
assert.equal(viewerContext.canOperateKuaishouIndustryVoucher, true)
|
||||
assert.equal(viewerContext.canManageTaskLifecycle, false)
|
||||
})
|
||||
|
||||
test('mapKuaishouCloudFulfillmentContext exposes cloud account display labels', () => {
|
||||
const flow = mapKuaishouCloudFulfillmentContext(
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@ export type AdminViewerContext = {
|
||||
canViewSensitiveTaskData: boolean
|
||||
canManageTaskLifecycle: boolean
|
||||
canOperateAssistedTask: boolean
|
||||
canOperateKuaishouIndustryVoucher: boolean
|
||||
}
|
||||
|
||||
type RedeemResolutionAttempt = {
|
||||
@@ -370,6 +371,7 @@ export function createAdminViewerContext(
|
||||
canViewSensitiveTaskData: role === 'admin' || role === 'operator',
|
||||
canManageTaskLifecycle: role === 'admin' || role === 'operator',
|
||||
canOperateAssistedTask: role === 'admin' || role === 'operator' || role === 'support',
|
||||
canOperateKuaishouIndustryVoucher: role === 'admin' || role === 'operator' || role === 'support',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -369,7 +369,7 @@ export async function resendAdminTaskKuaishouIndustryVoucherCode(
|
||||
const now = nowIso()
|
||||
const viewerContext = createAdminViewerContext(session)
|
||||
|
||||
if (!viewerContext.canManageTaskLifecycle) {
|
||||
if (!viewerContext.canOperateKuaishouIndustryVoucher) {
|
||||
throw createHttpError('当前账号没有权限重发电子凭证发码回调', {
|
||||
statusCode: 403,
|
||||
errorCode: 'admin_task_kuaishou_industry_resend_forbidden',
|
||||
@@ -446,7 +446,7 @@ export async function resendAdminOrderKuaishouIndustryVoucherCodes(
|
||||
const viewerContext = createAdminViewerContext(session)
|
||||
const now = nowIso()
|
||||
|
||||
if (!viewerContext.canManageTaskLifecycle) {
|
||||
if (!viewerContext.canOperateKuaishouIndustryVoucher) {
|
||||
throw createHttpError('当前账号没有权限重发电子凭证发码回调', {
|
||||
statusCode: 403,
|
||||
errorCode: 'admin_order_kuaishou_industry_resend_forbidden',
|
||||
@@ -519,7 +519,7 @@ export async function consumeAdminTaskKuaishouIndustryVoucher(
|
||||
const now = nowIso()
|
||||
const viewerContext = createAdminViewerContext(session)
|
||||
|
||||
if (!viewerContext.canManageTaskLifecycle) {
|
||||
if (!viewerContext.canOperateKuaishouIndustryVoucher) {
|
||||
throw createHttpError('当前账号没有权限执行电子凭证核销', {
|
||||
statusCode: 403,
|
||||
errorCode: 'admin_task_kuaishou_industry_consume_forbidden',
|
||||
|
||||
Generated
-30
@@ -995,9 +995,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1015,9 +1012,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1035,9 +1029,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1055,9 +1046,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1075,9 +1063,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1095,9 +1080,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1991,9 +1973,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2015,9 +1994,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2039,9 +2015,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2063,9 +2036,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import type { TableColumnsType, TabsProps } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
@@ -47,6 +47,7 @@ import type {
|
||||
AdminKuaishouIndustryVoucher,
|
||||
AdminKuaishouIndustryVoucherToolPayload,
|
||||
} from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { stringifyDisplayJson } from '@/utils/date-time'
|
||||
|
||||
@@ -78,6 +79,7 @@ const DEFAULT_ETICKET_TYPE = 'DINING_OPEN_TICKET'
|
||||
|
||||
export default function AdminKuaishouIndustryPage() {
|
||||
const [activeTab, setActiveTab] = useState<IndustryTab>('vouchers')
|
||||
const canManageRefund = hasAdminRole('operator')
|
||||
const [voucherLoading, setVoucherLoading] = useState(false)
|
||||
const [actionLoading, setActionLoading] = useState('')
|
||||
const [vouchers, setVouchers] = useState<AdminKuaishouIndustryVoucher[]>([])
|
||||
@@ -475,30 +477,39 @@ export default function AdminKuaishouIndustryPage() {
|
||||
showSuccess('已填入退款操作区')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-stack kuaishou-industry-page">
|
||||
<PageHeader title="快手电子凭证" description="行业电子凭证接口工具与售后处理" />
|
||||
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => setActiveTab(key as IndustryTab)}
|
||||
items={[
|
||||
{
|
||||
key: 'vouchers',
|
||||
label: '券码操作',
|
||||
children: renderVoucherTab(),
|
||||
},
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'vouchers',
|
||||
label: '券码操作',
|
||||
children: renderVoucherTab(),
|
||||
},
|
||||
...(canManageRefund
|
||||
? [
|
||||
{
|
||||
key: 'refunds',
|
||||
label: '售后退款',
|
||||
children: renderRefundTab(),
|
||||
},
|
||||
{
|
||||
key: 'result',
|
||||
label: '接口结果',
|
||||
children: renderResultTab(),
|
||||
},
|
||||
]}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: 'result',
|
||||
label: '接口结果',
|
||||
children: renderResultTab(),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="page-stack kuaishou-industry-page">
|
||||
<PageHeader
|
||||
title="快手电子凭证"
|
||||
description={canManageRefund ? '行业电子凭证接口工具与售后处理' : '行业电子凭证接口工具'}
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => setActiveTab(key as IndustryTab)}
|
||||
items={tabItems}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -87,6 +87,7 @@ export default function AdminTaskDetailPage() {
|
||||
})
|
||||
const detail = query.data?.data
|
||||
const canManageTaskLifecycle = hasAdminRole('operator')
|
||||
const canOperateIndustryVoucher = hasAdminRole('support')
|
||||
const canCloseTasks = hasAdminRole('support')
|
||||
const claimTokenStatus = String(detail?.claimToken?.status || '').trim()
|
||||
const isExternalClaimLink = claimTokenStatus.toLowerCase() === 'external'
|
||||
@@ -266,6 +267,7 @@ export default function AdminTaskDetailPage() {
|
||||
<TaskActionPanel
|
||||
detail={resolvedDetail}
|
||||
canManageTaskLifecycle={canManageTaskLifecycle}
|
||||
canOperateIndustryVoucher={canOperateIndustryVoucher}
|
||||
canCloseTasks={canCloseTasks}
|
||||
actionLoadingKey={actionLoadingKey}
|
||||
onRetry={() =>
|
||||
@@ -490,6 +492,7 @@ export default function AdminTaskDetailPage() {
|
||||
function TaskActionPanel({
|
||||
detail,
|
||||
canManageTaskLifecycle,
|
||||
canOperateIndustryVoucher,
|
||||
canCloseTasks,
|
||||
actionLoadingKey,
|
||||
onRetry,
|
||||
@@ -504,6 +507,7 @@ function TaskActionPanel({
|
||||
}: {
|
||||
detail: AdminTaskDetail
|
||||
canManageTaskLifecycle: boolean
|
||||
canOperateIndustryVoucher: boolean
|
||||
canCloseTasks: boolean
|
||||
actionLoadingKey: string
|
||||
onRetry: () => void
|
||||
@@ -607,7 +611,7 @@ function TaskActionPanel({
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{canManageTaskLifecycle && hasIndustryVoucherActions ? (
|
||||
{canOperateIndustryVoucher && hasIndustryVoucherActions ? (
|
||||
<section className="task-action-group">
|
||||
<Typography.Text type="secondary">电子凭证</Typography.Text>
|
||||
<Space wrap>
|
||||
@@ -648,7 +652,7 @@ function TaskActionPanel({
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{!canManageTaskLifecycle && !canCloseTasks && !hasCloudActions ? (
|
||||
{!canManageTaskLifecycle && !canOperateIndustryVoucher && !canCloseTasks && !hasCloudActions ? (
|
||||
<Typography.Text type="secondary">当前账号没有可执行的任务操作</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user