d306ffaea8
总部可按成功百分比拦截现场提货、配送下单,以及扫码、手机号和弱网核销,规则写入 v4 文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
431 lines
13 KiB
TypeScript
431 lines
13 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import {
|
|
Button,
|
|
Card,
|
|
Form,
|
|
InputNumber,
|
|
Modal,
|
|
Popconfirm,
|
|
Select,
|
|
Space,
|
|
Switch,
|
|
Table,
|
|
Typography,
|
|
message,
|
|
} from 'antd';
|
|
import {
|
|
API_ACCESS_ERROR_KINDS,
|
|
API_ACCESS_ERROR_LABELS,
|
|
API_ACCESS_PERCENT_FEATURES,
|
|
API_ACCESS_PERCENT_FEATURE_LABELS,
|
|
type ApiAccessActorHit,
|
|
type ApiAccessErrorKind,
|
|
type ApiAccessFormResponse,
|
|
type ApiAccessPercentFeature,
|
|
type ApiAccessPolicyDto,
|
|
} from '@dukang/shared-types';
|
|
import { request } from '../lib/api';
|
|
|
|
const ERROR_OPTIONS = API_ACCESS_ERROR_KINDS.map((value) => ({
|
|
value,
|
|
label: API_ACCESS_ERROR_LABELS[value],
|
|
}));
|
|
|
|
const FEATURE_OPTIONS = [
|
|
{ value: '', label: '全部功能' },
|
|
...API_ACCESS_PERCENT_FEATURES.map((value) => ({
|
|
value,
|
|
label: API_ACCESS_PERCENT_FEATURE_LABELS[value],
|
|
})),
|
|
];
|
|
|
|
type PercentDraft = {
|
|
featureKey: ApiAccessPercentFeature;
|
|
featureLabel: string;
|
|
successPercent: number;
|
|
errorKind: ApiAccessErrorKind;
|
|
};
|
|
|
|
export default function ApiAccessPage() {
|
|
const [loading, setLoading] = useState(false);
|
|
const [savingGlobals, setSavingGlobals] = useState(false);
|
|
const [savingNotifies, setSavingNotifies] = useState(false);
|
|
const [percents, setPercents] = useState<PercentDraft[]>([]);
|
|
const [overrides, setOverrides] = useState<ApiAccessPolicyDto[]>([]);
|
|
const [notifies, setNotifies] = useState<ApiAccessPolicyDto[]>([]);
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await request<ApiAccessFormResponse>('/admin/api-access');
|
|
setPercents(
|
|
data.percents.map((row) => ({
|
|
featureKey: row.featureKey as ApiAccessPercentFeature,
|
|
featureLabel: row.featureLabel,
|
|
successPercent: row.successPercent ?? 100,
|
|
errorKind: row.errorKind ?? 'request_error',
|
|
})),
|
|
);
|
|
setOverrides(data.overrides);
|
|
setNotifies(data.notifies);
|
|
} catch (error) {
|
|
message.error(error instanceof Error ? error.message : '加载失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
void load();
|
|
}, [load]);
|
|
|
|
async function saveGlobals() {
|
|
setSavingGlobals(true);
|
|
try {
|
|
const data = await request<ApiAccessFormResponse>('/admin/api-access/globals', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
items: percents.map((row) => ({
|
|
featureKey: row.featureKey,
|
|
successPercent: row.successPercent,
|
|
errorKind: row.errorKind,
|
|
})),
|
|
}),
|
|
});
|
|
setOverrides(data.overrides);
|
|
message.success('已保存成功百分比');
|
|
} catch (error) {
|
|
message.error(error instanceof Error ? error.message : '保存失败');
|
|
} finally {
|
|
setSavingGlobals(false);
|
|
}
|
|
}
|
|
|
|
async function saveNotifies() {
|
|
setSavingNotifies(true);
|
|
try {
|
|
await request<ApiAccessFormResponse>('/admin/api-access/notifies', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
items: notifies.map((row) => ({ featureKey: row.featureKey, enabled: row.enabled })),
|
|
}),
|
|
});
|
|
message.success('已保存通知开关');
|
|
} catch (error) {
|
|
message.error(error instanceof Error ? error.message : '保存失败');
|
|
} finally {
|
|
setSavingNotifies(false);
|
|
}
|
|
}
|
|
|
|
async function removeOverride(id: string) {
|
|
try {
|
|
const data = await request<ApiAccessFormResponse>(`/admin/api-access/overrides/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
setOverrides(data.overrides);
|
|
message.success('已删除');
|
|
} catch (error) {
|
|
message.error(error instanceof Error ? error.message : '删除失败');
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
|
<div>
|
|
<Typography.Title level={4} style={{ marginBottom: 8 }}>
|
|
接口访问
|
|
</Typography.Title>
|
|
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
|
成功百分比是放行比例,默认 100。0 为全部拒绝,每次请求单独计算。可按用户或账户覆盖。总部登录不受登录百分比影响。被拒绝的请求只返回所选文案,不发企微。通知开关只停止对应企微消息,不拦截下单、核销、出账、审核和改套餐。
|
|
</Typography.Paragraph>
|
|
</div>
|
|
|
|
<Card
|
|
title="功能成功百分比"
|
|
extra={
|
|
<Button type="primary" loading={savingGlobals} onClick={() => void saveGlobals()}>
|
|
保存
|
|
</Button>
|
|
}
|
|
>
|
|
<Table
|
|
rowKey="featureKey"
|
|
loading={loading}
|
|
pagination={false}
|
|
dataSource={percents}
|
|
columns={[
|
|
{ title: '功能', dataIndex: 'featureLabel', width: 140 },
|
|
{
|
|
title: '成功百分比',
|
|
width: 180,
|
|
render: (_, row) => (
|
|
<InputNumber
|
|
min={0}
|
|
max={100}
|
|
precision={0}
|
|
value={row.successPercent}
|
|
onChange={(value) =>
|
|
setPercents((list) =>
|
|
list.map((item) =>
|
|
item.featureKey === row.featureKey
|
|
? { ...item, successPercent: typeof value === 'number' ? value : 100 }
|
|
: item,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: '失败文案',
|
|
render: (_, row) => (
|
|
<Select
|
|
style={{ width: 180 }}
|
|
value={row.errorKind}
|
|
options={ERROR_OPTIONS}
|
|
onChange={(errorKind: ApiAccessErrorKind) =>
|
|
setPercents((list) =>
|
|
list.map((item) => (item.featureKey === row.featureKey ? { ...item, errorKind } : item)),
|
|
)
|
|
}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: '说明',
|
|
render: (_, row) =>
|
|
row.featureKey === 'audit_notify'
|
|
? '不拦截审核,只按比例丢弃门店审核通知'
|
|
: row.featureKey === 'login'
|
|
? 'C 端、门店、合伙人登录;总部登录始终放行'
|
|
: row.featureKey === 'order_create'
|
|
? '现场提货和选择地址配送都拦截,并返回失败文案'
|
|
: row.featureKey === 'redeem_confirm'
|
|
? '扫码核销、手机号核销和弱网待处理都拦截,并返回失败文案'
|
|
: '命中后直接返回失败文案',
|
|
},
|
|
]}
|
|
/>
|
|
</Card>
|
|
|
|
<Card
|
|
title="账户 / 用户覆盖"
|
|
extra={
|
|
<Button type="primary" onClick={() => setOpen(true)}>
|
|
添加
|
|
</Button>
|
|
}
|
|
>
|
|
<Table
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={false}
|
|
dataSource={overrides}
|
|
locale={{ emptyText: '未设置覆盖,全部走上方的功能百分比' }}
|
|
columns={[
|
|
{
|
|
title: '范围',
|
|
width: 100,
|
|
render: (_, row) => (row.scopeType === 'user' ? '用户' : '账户'),
|
|
},
|
|
{
|
|
title: '对象',
|
|
render: (_, row) => row.actorLabel || `${row.actorType} ${row.actorId}`,
|
|
},
|
|
{ title: '功能', dataIndex: 'featureLabel', width: 140 },
|
|
{ title: '成功百分比', dataIndex: 'successPercent', width: 120 },
|
|
{
|
|
title: '失败文案',
|
|
width: 160,
|
|
render: (_, row) => (row.errorKind ? API_ACCESS_ERROR_LABELS[row.errorKind] : '请求异常'),
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 100,
|
|
render: (_, row) => (
|
|
<Popconfirm title="删除这条覆盖?" onConfirm={() => void removeOverride(row.id)}>
|
|
<Button type="link" danger>
|
|
删除
|
|
</Button>
|
|
</Popconfirm>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</Card>
|
|
|
|
<Card
|
|
title="通知开关"
|
|
extra={
|
|
<Button type="primary" loading={savingNotifies} onClick={() => void saveNotifies()}>
|
|
保存
|
|
</Button>
|
|
}
|
|
>
|
|
<Table
|
|
rowKey="featureKey"
|
|
loading={loading}
|
|
pagination={false}
|
|
dataSource={notifies}
|
|
columns={[
|
|
{ title: '通知', dataIndex: 'featureLabel' },
|
|
{
|
|
title: '发送企微',
|
|
width: 160,
|
|
render: (_, row) => (
|
|
<Switch
|
|
checked={row.enabled}
|
|
checkedChildren="开"
|
|
unCheckedChildren="关"
|
|
onChange={(enabled) =>
|
|
setNotifies((list) =>
|
|
list.map((item) => (item.featureKey === row.featureKey ? { ...item, enabled } : item)),
|
|
)
|
|
}
|
|
/>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</Card>
|
|
|
|
<OverrideModal
|
|
open={open}
|
|
onClose={() => setOpen(false)}
|
|
onCreated={(data) => {
|
|
setOverrides(data.overrides);
|
|
setOpen(false);
|
|
}}
|
|
/>
|
|
</Space>
|
|
);
|
|
}
|
|
|
|
function OverrideModal({
|
|
open,
|
|
onClose,
|
|
onCreated,
|
|
}: {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onCreated: (data: ApiAccessFormResponse) => void;
|
|
}) {
|
|
const [form] = Form.useForm();
|
|
const [scope, setScope] = useState<'user' | 'account'>('user');
|
|
const [hits, setHits] = useState<ApiAccessActorHit[]>([]);
|
|
const [searching, setSearching] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
form.resetFields();
|
|
form.setFieldsValue({
|
|
scopeType: 'user',
|
|
featureKey: '',
|
|
successPercent: 100,
|
|
errorKind: 'request_error',
|
|
});
|
|
setScope('user');
|
|
setHits([]);
|
|
}, [open, form]);
|
|
|
|
async function onSearch(q: string) {
|
|
const keyword = q.trim();
|
|
if (!keyword) {
|
|
setHits([]);
|
|
return;
|
|
}
|
|
setSearching(true);
|
|
try {
|
|
const rows = await request<ApiAccessActorHit[]>(
|
|
`/admin/api-access/actors?scope=${scope}&q=${encodeURIComponent(keyword)}`,
|
|
);
|
|
setHits(rows);
|
|
} catch (error) {
|
|
message.error(error instanceof Error ? error.message : '搜索失败');
|
|
} finally {
|
|
setSearching(false);
|
|
}
|
|
}
|
|
|
|
async function submit() {
|
|
const values = await form.validateFields();
|
|
const hit = hits.find((item) => `${item.actorType}:${item.actorId}` === values.actor);
|
|
if (!hit) {
|
|
message.error('请选择对象');
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
const data = await request<ApiAccessFormResponse>('/admin/api-access/overrides', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
scopeType: scope,
|
|
actorType: hit.actorType,
|
|
actorId: hit.actorId,
|
|
featureKey: values.featureKey || null,
|
|
successPercent: values.successPercent,
|
|
errorKind: values.errorKind,
|
|
}),
|
|
});
|
|
message.success('已添加');
|
|
onCreated(data);
|
|
} catch (error) {
|
|
message.error(error instanceof Error ? error.message : '保存失败');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
title="添加覆盖"
|
|
open={open}
|
|
onCancel={onClose}
|
|
onOk={() => void submit()}
|
|
confirmLoading={saving}
|
|
destroyOnClose
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item name="scopeType" label="范围" rules={[{ required: true }]}>
|
|
<Select
|
|
options={[
|
|
{ value: 'user', label: '用户' },
|
|
{ value: 'account', label: '账户' },
|
|
]}
|
|
onChange={(value: 'user' | 'account') => {
|
|
setScope(value);
|
|
setHits([]);
|
|
form.setFieldValue('actor', undefined);
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item name="actor" label="对象" rules={[{ required: true, message: '请选择对象' }]}>
|
|
<Select
|
|
showSearch
|
|
filterOption={false}
|
|
placeholder={scope === 'user' ? '手机号、昵称或用户编号' : '手机号或名称'}
|
|
loading={searching}
|
|
onSearch={(q) => void onSearch(q)}
|
|
options={hits.map((hit) => ({
|
|
value: `${hit.actorType}:${hit.actorId}`,
|
|
label: hit.label,
|
|
}))}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item name="featureKey" label="功能">
|
|
<Select options={FEATURE_OPTIONS} />
|
|
</Form.Item>
|
|
<Form.Item name="successPercent" label="成功百分比" rules={[{ required: true }]}>
|
|
<InputNumber min={0} max={100} precision={0} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item name="errorKind" label="失败文案" rules={[{ required: true }]}>
|
|
<Select options={ERROR_OPTIONS} />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
);
|
|
}
|