chore: commit pending gateway changes

This commit is contained in:
2026-05-10 22:34:15 +08:00
parent 53f8edfb67
commit d59756a27c
71 changed files with 15106 additions and 656 deletions
@@ -0,0 +1,775 @@
import { useMemo, useState, type FormEvent, type ReactNode } from 'react';
import { Building2, KeyRound, Pencil, Plus, RotateCcw, Trash2, UserRound, UsersRound } from 'lucide-react';
import type {
GatewayTenant,
GatewayTenantUpsertRequest,
GatewayUser,
GatewayUserUpsertRequest,
UserGroup,
UserGroupUpsertRequest,
} from '@easyai-ai-gateway/contracts';
import {
Badge,
Button,
Card,
CardContent,
CardHeader,
CardTitle,
ConfirmDialog,
FormDialog,
Input,
Label,
Select,
Table,
TableCell,
TableHead,
TableRow,
Textarea,
} from '../../components/ui';
import type { ConsoleData } from '../../app-state';
import type { LoadState } from '../../types';
type TenantForm = {
tenantKey: string;
source: string;
externalTenantId: string;
name: string;
description: string;
defaultUserGroupId: string;
planKey: string;
billingProfileJson: string;
rateLimitPolicyJson: string;
authPolicyJson: string;
metadataJson: string;
status: string;
};
type UserForm = {
userKey: string;
source: string;
externalUserId: string;
username: string;
displayName: string;
email: string;
phone: string;
avatarUrl: string;
password: string;
gatewayTenantId: string;
tenantId: string;
tenantKey: string;
defaultUserGroupId: string;
roles: string;
authProfileJson: string;
metadataJson: string;
status: string;
};
type UserGroupForm = {
groupKey: string;
name: string;
description: string;
source: string;
priority: string;
rechargeDiscountPolicyJson: string;
billingDiscountPolicyJson: string;
rateLimitPolicyJson: string;
quotaPolicyJson: string;
metadataJson: string;
status: string;
};
const tenantStatuses = ['active', 'disabled', 'locked'];
const userStatuses = ['active', 'disabled', 'locked'];
const userGroupStatuses = ['active', 'disabled'];
const sourceOptions = ['gateway', 'server-main', 'sync'];
const roleOptions = [
{ label: '普通用户', value: 'user' },
{ label: '创作者', value: 'creator' },
{ label: '管理', value: 'operator' },
{ label: '超级管理员', value: 'manager' },
{ label: '管理员', value: 'admin' },
];
export function TenantsPanel(props: IdentityPanelProps) {
const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState('');
const [form, setForm] = useState<TenantForm>(() => defaultTenantForm());
const [localError, setLocalError] = useState('');
const [pendingDeleteTenant, setPendingDeleteTenant] = useState<GatewayTenant | null>(null);
function openCreateDialog() {
setEditingId('');
setLocalError('');
setForm(defaultTenantForm());
setDialogOpen(true);
}
function editTenant(tenant: GatewayTenant) {
setEditingId(tenant.id);
setLocalError('');
setForm(tenantToForm(tenant));
setDialogOpen(true);
}
function closeDialog() {
setDialogOpen(false);
setEditingId('');
setLocalError('');
}
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setLocalError('');
try {
await props.onSaveTenant(formToTenantPayload(form), editingId || undefined);
closeDialog();
} catch (err) {
setLocalError(err instanceof Error ? err.message : '保存租户失败');
}
}
async function deleteTenant(tenant: GatewayTenant) {
try {
await props.onDeleteTenant(tenant.id);
setPendingDeleteTenant(null);
} catch (err) {
setLocalError(err instanceof Error ? err.message : '删除租户失败');
}
}
return (
<div className="pageStack">
<IdentityHeader
count={props.data.tenants.length}
description="租户可独立维护,也可承载 server-main 同步过来的租户标识。"
icon={<Building2 size={17} />}
message={localError || props.operationMessage}
title="租户管理"
actionLabel="新增租户"
onCreate={openCreateDialog}
/>
{props.data.tenants.length ? (
<Table className="identityDataTable tenantTable">
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
{props.data.tenants.map((tenant) => (
<TableRow key={tenant.id}>
<TableCell><IdentityName title={tenant.name} subtitle={tenant.tenantKey} /></TableCell>
<TableCell>{tenant.source}</TableCell>
<TableCell>{groupName(props.data.userGroups, tenant.defaultUserGroupId)}</TableCell>
<TableCell>{tenant.planKey || '未设置'}</TableCell>
<TableCell><Badge variant={tenant.status === 'active' ? 'success' : 'secondary'}>{tenant.status}</Badge></TableCell>
<TableCell><TableActions onEdit={() => editTenant(tenant)} onDelete={() => setPendingDeleteTenant(tenant)} /></TableCell>
</TableRow>
))}
</Table>
) : (
<EmptyIdentity title="暂无租户" description="点击新增租户建立本地租户闭环。" />
)}
<FormDialog
ariaLabel={editingId ? '编辑租户' : '新增租户'}
className="identityDialog"
eyebrow={editingId ? 'Edit Tenant' : 'New Tenant'}
footer={<DialogFooter editing={Boolean(editingId)} loading={props.state === 'loading'} onCancel={closeDialog} />}
open={dialogOpen}
title={editingId ? '编辑租户' : '新增租户'}
onClose={closeDialog}
onSubmit={submit}
>
<Label> Key<Input size="sm" value={form.tenantKey} onChange={(event) => setForm({ ...form, tenantKey: event.target.value })} placeholder="default" /></Label>
<Label><Input size="sm" value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} placeholder="默认租户" /></Label>
<Label><Select size="sm" value={form.source} onChange={(event) => setForm({ ...form, source: event.target.value })}>{sourceOptions.map(option)}</Select></Label>
<Label><Select size="sm" value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value })}>{tenantStatuses.map(option)}</Select></Label>
<Label> ID<Input size="sm" value={form.externalTenantId} onChange={(event) => setForm({ ...form, externalTenantId: event.target.value })} placeholder="server-main tenant id" /></Label>
<Label> Key<Input size="sm" value={form.planKey} onChange={(event) => setForm({ ...form, planKey: event.target.value })} placeholder="free / pro" /></Label>
<Label className="spanTwo"><Select size="sm" value={form.defaultUserGroupId} onChange={(event) => setForm({ ...form, defaultUserGroupId: event.target.value })}><option value=""></option>{props.data.userGroups.map((group) => <option value={group.id} key={group.id}>{group.name}</option>)}</Select></Label>
<Label className="spanTwo"><Input size="sm" value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} placeholder="内部备注" /></Label>
<JsonField label="计费资料 JSON" value={form.billingProfileJson} onChange={(value) => setForm({ ...form, billingProfileJson: value })} />
<JsonField label="限流策略 JSON" value={form.rateLimitPolicyJson} onChange={(value) => setForm({ ...form, rateLimitPolicyJson: value })} />
<JsonField label="授权策略 JSON" value={form.authPolicyJson} onChange={(value) => setForm({ ...form, authPolicyJson: value })} />
<JsonField label="元数据 JSON" value={form.metadataJson} onChange={(value) => setForm({ ...form, metadataJson: value })} />
</FormDialog>
<ConfirmDialog
confirmLabel="删除租户"
description="租户会被软删除,关联用户不会被自动删除。"
loading={props.state === 'loading'}
open={Boolean(pendingDeleteTenant)}
title={`确认删除租户 ${pendingDeleteTenant?.name ?? ''}`}
onCancel={() => setPendingDeleteTenant(null)}
onConfirm={() => pendingDeleteTenant ? deleteTenant(pendingDeleteTenant) : undefined}
/>
</div>
);
}
export function UsersPanel(props: IdentityPanelProps) {
const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState('');
const [form, setForm] = useState<UserForm>(() => defaultUserForm(props.data.tenants[0]));
const [localError, setLocalError] = useState('');
const [pendingDeleteUser, setPendingDeleteUser] = useState<GatewayUser | null>(null);
const tenantById = useMemo(() => new Map(props.data.tenants.map((tenant) => [tenant.id, tenant])), [props.data.tenants]);
function openCreateDialog() {
setEditingId('');
setLocalError('');
setForm(defaultUserForm(props.data.tenants[0]));
setDialogOpen(true);
}
function editUser(user: GatewayUser) {
setEditingId(user.id);
setLocalError('');
setForm(userToForm(user));
setDialogOpen(true);
}
function closeDialog() {
setDialogOpen(false);
setEditingId('');
setLocalError('');
}
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setLocalError('');
try {
await props.onSaveUser(formToUserPayload(form), editingId || undefined);
closeDialog();
} catch (err) {
setLocalError(err instanceof Error ? err.message : '保存用户失败');
}
}
async function deleteUser(user: GatewayUser) {
try {
await props.onDeleteUser(user.id);
setPendingDeleteUser(null);
} catch (err) {
setLocalError(err instanceof Error ? err.message : '删除用户失败');
}
}
function selectTenant(gatewayTenantId: string) {
const tenant = tenantById.get(gatewayTenantId);
setForm({ ...form, gatewayTenantId, tenantKey: tenant?.tenantKey ?? form.tenantKey });
}
return (
<div className="pageStack">
<IdentityHeader
count={props.data.users.length}
description="支持本地用户闭环,也保留 server-main 用户同步字段。"
icon={<UserRound size={17} />}
message={localError || props.operationMessage}
title="用户管理"
actionLabel="新增用户"
onCreate={openCreateDialog}
/>
{props.data.users.length ? (
<Table className="identityDataTable userTable">
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
{props.data.users.map((user) => (
<TableRow key={user.id}>
<TableCell><IdentityName title={user.displayName || user.username} subtitle={user.email || user.username} /></TableCell>
<TableCell>{roleLabel(user.roles)}</TableCell>
<TableCell>{tenantName(props.data.tenants, user.gatewayTenantId, user.tenantKey)}</TableCell>
<TableCell>{groupName(props.data.userGroups, user.defaultUserGroupId)}</TableCell>
<TableCell>{user.source}</TableCell>
<TableCell><Badge variant={user.status === 'active' ? 'success' : 'secondary'}>{user.status}</Badge></TableCell>
<TableCell><TableActions onEdit={() => editUser(user)} onDelete={() => setPendingDeleteUser(user)} /></TableCell>
</TableRow>
))}
</Table>
) : (
<EmptyIdentity title="暂无用户" description="可先添加本地用户,后续再与 server-main 同步关系对齐。" />
)}
<FormDialog
ariaLabel={editingId ? '编辑用户' : '新增用户'}
className="identityDialog"
eyebrow={editingId ? 'Edit User' : 'New User'}
footer={<DialogFooter editing={Boolean(editingId)} loading={props.state === 'loading'} onCancel={closeDialog} />}
open={dialogOpen}
title={editingId ? '编辑用户' : '新增用户'}
onClose={closeDialog}
onSubmit={submit}
>
<Label><Input size="sm" value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} placeholder="user@example.com" /></Label>
<Label> Key<Input size="sm" value={form.userKey} onChange={(event) => setForm({ ...form, userKey: event.target.value })} placeholder="留空自动生成" /></Label>
<Label><Input size="sm" value={form.displayName} onChange={(event) => setForm({ ...form, displayName: event.target.value })} placeholder="王小明" /></Label>
<Label><Input size="sm" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} placeholder="name@example.com" /></Label>
<Label><Input size="sm" value={form.phone} onChange={(event) => setForm({ ...form, phone: event.target.value })} /></Label>
<Label> URL<Input size="sm" value={form.avatarUrl} onChange={(event) => setForm({ ...form, avatarUrl: event.target.value })} /></Label>
<Label><Select size="sm" value={form.source} onChange={(event) => setForm({ ...form, source: event.target.value })}>{sourceOptions.map(option)}</Select></Label>
<Label><Select size="sm" value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value })}>{userStatuses.map(option)}</Select></Label>
<Label><Select size="sm" value={form.gatewayTenantId} onChange={(event) => selectTenant(event.target.value)}><option value=""></option>{props.data.tenants.map((tenant) => <option value={tenant.id} key={tenant.id}>{tenant.name}</option>)}</Select></Label>
<Label> Key<Input size="sm" value={form.tenantKey} onChange={(event) => setForm({ ...form, tenantKey: event.target.value })} /></Label>
<Label> ID<Input size="sm" value={form.externalUserId} onChange={(event) => setForm({ ...form, externalUserId: event.target.value })} /></Label>
<Label> ID<Input size="sm" value={form.tenantId} onChange={(event) => setForm({ ...form, tenantId: event.target.value })} /></Label>
<Label><Select size="sm" value={form.defaultUserGroupId} onChange={(event) => setForm({ ...form, defaultUserGroupId: event.target.value })}><option value=""></option>{props.data.userGroups.map((group) => <option value={group.id} key={group.id}>{group.name}</option>)}</Select></Label>
<Label><Input size="sm" value={form.password} type="password" onChange={(event) => setForm({ ...form, password: event.target.value })} placeholder={editingId ? '留空不修改' : '可为空'} /></Label>
<Label>
<Select size="sm" value={form.roles} onChange={(event) => setForm({ ...form, roles: event.target.value })}>
{roleOptions.map((role) => <option value={role.value} key={role.value}>{role.label}</option>)}
</Select>
</Label>
<JsonField label="授权资料 JSON" value={form.authProfileJson} onChange={(value) => setForm({ ...form, authProfileJson: value })} />
<JsonField label="元数据 JSON" value={form.metadataJson} onChange={(value) => setForm({ ...form, metadataJson: value })} />
</FormDialog>
<ConfirmDialog
confirmLabel="删除用户"
description="用户会被软删除,已创建的任务记录会保留。"
loading={props.state === 'loading'}
open={Boolean(pendingDeleteUser)}
title={`确认删除用户 ${pendingDeleteUser?.username ?? ''}`}
onCancel={() => setPendingDeleteUser(null)}
onConfirm={() => pendingDeleteUser ? deleteUser(pendingDeleteUser) : undefined}
/>
</div>
);
}
export function UserGroupsPanel(props: IdentityPanelProps) {
const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState('');
const [form, setForm] = useState<UserGroupForm>(() => defaultUserGroupForm());
const [localError, setLocalError] = useState('');
const [pendingDeleteGroup, setPendingDeleteGroup] = useState<UserGroup | null>(null);
function openCreateDialog() {
setEditingId('');
setLocalError('');
setForm(defaultUserGroupForm());
setDialogOpen(true);
}
function editGroup(group: UserGroup) {
setEditingId(group.id);
setLocalError('');
setForm(userGroupToForm(group));
setDialogOpen(true);
}
function closeDialog() {
setDialogOpen(false);
setEditingId('');
setLocalError('');
}
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setLocalError('');
try {
await props.onSaveUserGroup(formToUserGroupPayload(form), editingId || undefined);
closeDialog();
} catch (err) {
setLocalError(err instanceof Error ? err.message : '保存用户组失败');
}
}
async function deleteGroup(group: UserGroup) {
try {
await props.onDeleteUserGroup(group.id);
setPendingDeleteGroup(null);
} catch (err) {
setLocalError(err instanceof Error ? err.message : '删除用户组失败');
}
}
return (
<div className="pageStack">
<IdentityHeader
count={props.data.userGroups.length}
description="用户组承载充值折扣、计费折扣、限流和额度策略。"
icon={<UsersRound size={17} />}
message={localError || props.operationMessage}
title="用户组管理"
actionLabel="新增用户组"
onCreate={openCreateDialog}
/>
{props.data.userGroups.length ? (
<Table className="identityDataTable groupTable">
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
{props.data.userGroups.map((group) => (
<TableRow key={group.id}>
<TableCell><IdentityName title={group.name} subtitle={group.groupKey} /></TableCell>
<TableCell>{group.source}</TableCell>
<TableCell>{group.priority}</TableCell>
<TableCell>{discountSummary(group)}</TableCell>
<TableCell>{policyKeys(group.rateLimitPolicy).join(', ') || '未设置'}</TableCell>
<TableCell><Badge variant={group.status === 'active' ? 'success' : 'secondary'}>{group.status}</Badge></TableCell>
<TableCell><TableActions onEdit={() => editGroup(group)} onDelete={() => setPendingDeleteGroup(group)} /></TableCell>
</TableRow>
))}
</Table>
) : (
<EmptyIdentity title="暂无用户组" description="可先新增默认用户组,用来配置折扣和并发控制。" />
)}
<FormDialog
ariaLabel={editingId ? '编辑用户组' : '新增用户组'}
className="identityDialog"
eyebrow={editingId ? 'Edit User Group' : 'New User Group'}
footer={<DialogFooter editing={Boolean(editingId)} loading={props.state === 'loading'} onCancel={closeDialog} />}
open={dialogOpen}
title={editingId ? '编辑用户组' : '新增用户组'}
onClose={closeDialog}
onSubmit={submit}
>
<Label> Key<Input size="sm" value={form.groupKey} onChange={(event) => setForm({ ...form, groupKey: event.target.value })} placeholder="default" /></Label>
<Label><Input size="sm" value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} placeholder="默认用户组" /></Label>
<Label><Select size="sm" value={form.source} onChange={(event) => setForm({ ...form, source: event.target.value })}>{sourceOptions.map(option)}</Select></Label>
<Label><Select size="sm" value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value })}>{userGroupStatuses.map(option)}</Select></Label>
<Label><Input size="sm" value={form.priority} inputMode="numeric" onChange={(event) => setForm({ ...form, priority: event.target.value })} /></Label>
<Label className="spanTwo"><Input size="sm" value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} /></Label>
<JsonField label="充值折扣策略 JSON" value={form.rechargeDiscountPolicyJson} onChange={(value) => setForm({ ...form, rechargeDiscountPolicyJson: value })} />
<JsonField label="计费折扣策略 JSON" value={form.billingDiscountPolicyJson} onChange={(value) => setForm({ ...form, billingDiscountPolicyJson: value })} />
<JsonField label="限流策略 JSON" value={form.rateLimitPolicyJson} onChange={(value) => setForm({ ...form, rateLimitPolicyJson: value })} />
<JsonField label="额度策略 JSON" value={form.quotaPolicyJson} onChange={(value) => setForm({ ...form, quotaPolicyJson: value })} />
<JsonField label="元数据 JSON" value={form.metadataJson} onChange={(value) => setForm({ ...form, metadataJson: value })} />
</FormDialog>
<ConfirmDialog
confirmLabel="删除用户组"
description="删除后租户和用户上引用该用户组的默认策略会失效。"
loading={props.state === 'loading'}
open={Boolean(pendingDeleteGroup)}
title={`确认删除用户组 ${pendingDeleteGroup?.name ?? ''}`}
onCancel={() => setPendingDeleteGroup(null)}
onConfirm={() => pendingDeleteGroup ? deleteGroup(pendingDeleteGroup) : undefined}
/>
</div>
);
}
type IdentityPanelProps = {
data: ConsoleData;
operationMessage: string;
state: LoadState;
onDeleteTenant: (tenantId: string) => Promise<void>;
onDeleteUser: (userId: string) => Promise<void>;
onDeleteUserGroup: (groupId: string) => Promise<void>;
onSaveTenant: (input: GatewayTenantUpsertRequest, tenantId?: string) => Promise<void>;
onSaveUser: (input: GatewayUserUpsertRequest, userId?: string) => Promise<void>;
onSaveUserGroup: (input: UserGroupUpsertRequest, groupId?: string) => Promise<void>;
};
function IdentityHeader(props: {
actionLabel: string;
count: number;
description: string;
icon: ReactNode;
message?: string;
title: string;
onCreate: () => void;
}) {
return (
<Card>
<CardHeader>
<div className="identityHeaderTitle">
<div className="iconBox">{props.icon}</div>
<div>
<CardTitle>{props.title}</CardTitle>
<p className="mutedText">{props.description}</p>
</div>
<Badge variant="secondary">{props.count}</Badge>
</div>
<Button type="button" onClick={props.onCreate}><Plus size={15} />{props.actionLabel}</Button>
</CardHeader>
{props.message && <CardContent><p className="formMessage">{props.message}</p></CardContent>}
</Card>
);
}
function IdentityName(props: { title: string; subtitle: string }) {
return (
<span className="identityTableName">
<strong>{props.title}</strong>
<small>{props.subtitle}</small>
</span>
);
}
function TableActions(props: { onDelete: () => void; onEdit: () => void }) {
return (
<span className="tableActions">
<Button type="button" variant="outline" size="icon" title="修改" onClick={props.onEdit}><Pencil size={14} /></Button>
<Button type="button" variant="destructive" size="icon" title="删除" onClick={props.onDelete}><Trash2 size={14} /></Button>
</span>
);
}
function EmptyIdentity(props: { title: string; description: string }) {
return (
<Card>
<CardContent className="emptyState">
<KeyRound size={18} />
<strong>{props.title}</strong>
<span>{props.description}</span>
</CardContent>
</Card>
);
}
function DialogFooter(props: { editing: boolean; loading: boolean; onCancel: () => void }) {
return (
<>
<Button type="button" variant="outline" onClick={props.onCancel}><RotateCcw size={15} /></Button>
<Button type="submit" disabled={props.loading}>{props.editing ? <Pencil size={15} /> : <Plus size={15} />}{props.editing ? '保存修改' : '新增'}</Button>
</>
);
}
function JsonField(props: { label: string; value: string; onChange: (value: string) => void }) {
return (
<Label className="spanTwo">
{props.label}
<Textarea size="sm" rows={3} value={props.value} placeholder='{"rules":[]}' onChange={(event) => props.onChange(event.target.value)} />
</Label>
);
}
function option(value: string) {
return <option value={value} key={value}>{value}</option>;
}
function defaultTenantForm(): TenantForm {
return {
tenantKey: `tenant-${Date.now().toString(36)}`,
source: 'gateway',
externalTenantId: '',
name: '默认租户',
description: '',
defaultUserGroupId: '',
planKey: '',
billingProfileJson: '{}',
rateLimitPolicyJson: '{}',
authPolicyJson: '{}',
metadataJson: '{}',
status: 'active',
};
}
function tenantToForm(tenant: GatewayTenant): TenantForm {
return {
tenantKey: tenant.tenantKey,
source: tenant.source,
externalTenantId: tenant.externalTenantId ?? '',
name: tenant.name,
description: tenant.description ?? '',
defaultUserGroupId: tenant.defaultUserGroupId ?? '',
planKey: tenant.planKey ?? '',
billingProfileJson: stringifyJson(tenant.billingProfile),
rateLimitPolicyJson: stringifyJson(tenant.rateLimitPolicy),
authPolicyJson: stringifyJson(tenant.authPolicy),
metadataJson: stringifyJson(tenant.metadata),
status: tenant.status,
};
}
function formToTenantPayload(form: TenantForm): GatewayTenantUpsertRequest {
return {
tenantKey: form.tenantKey.trim(),
source: form.source,
externalTenantId: form.externalTenantId.trim() || undefined,
name: form.name.trim(),
description: form.description.trim() || undefined,
defaultUserGroupId: form.defaultUserGroupId || undefined,
planKey: form.planKey.trim() || undefined,
billingProfile: parseJsonObject(form.billingProfileJson, '计费资料 JSON'),
rateLimitPolicy: parseJsonObject(form.rateLimitPolicyJson, '限流策略 JSON'),
authPolicy: parseJsonObject(form.authPolicyJson, '授权策略 JSON'),
metadata: parseJsonObject(form.metadataJson, '元数据 JSON'),
status: form.status,
};
}
function defaultUserForm(tenant?: GatewayTenant): UserForm {
return {
userKey: '',
source: 'gateway',
externalUserId: '',
username: '',
displayName: '',
email: '',
phone: '',
avatarUrl: '',
password: '',
gatewayTenantId: tenant?.id ?? '',
tenantId: '',
tenantKey: tenant?.tenantKey ?? '',
defaultUserGroupId: '',
roles: 'user',
authProfileJson: '{}',
metadataJson: '{}',
status: 'active',
};
}
function userToForm(user: GatewayUser): UserForm {
return {
userKey: user.userKey,
source: user.source,
externalUserId: user.externalUserId ?? '',
username: user.username,
displayName: user.displayName ?? '',
email: user.email ?? '',
phone: user.phone ?? '',
avatarUrl: user.avatarUrl ?? '',
password: '',
gatewayTenantId: user.gatewayTenantId ?? '',
tenantId: user.tenantId ?? '',
tenantKey: user.tenantKey ?? '',
defaultUserGroupId: user.defaultUserGroupId ?? '',
roles: roleValue(user.roles),
authProfileJson: stringifyJson(user.authProfile),
metadataJson: stringifyJson(user.metadata),
status: user.status,
};
}
function formToUserPayload(form: UserForm): GatewayUserUpsertRequest {
return {
userKey: form.userKey.trim() || undefined,
source: form.source,
externalUserId: form.externalUserId.trim() || undefined,
username: form.username.trim(),
displayName: form.displayName.trim() || undefined,
email: form.email.trim() || undefined,
phone: form.phone.trim() || undefined,
avatarUrl: form.avatarUrl.trim() || undefined,
password: form.password || undefined,
gatewayTenantId: form.gatewayTenantId || undefined,
tenantId: form.tenantId.trim() || undefined,
tenantKey: form.tenantKey.trim() || undefined,
defaultUserGroupId: form.defaultUserGroupId || undefined,
roles: [roleValue([form.roles])],
authProfile: parseJsonObject(form.authProfileJson, '授权资料 JSON'),
metadata: parseJsonObject(form.metadataJson, '元数据 JSON'),
status: form.status,
};
}
function defaultUserGroupForm(): UserGroupForm {
return {
groupKey: `group-${Date.now().toString(36)}`,
name: '默认用户组',
description: '',
source: 'gateway',
priority: '100',
rechargeDiscountPolicyJson: '{}',
billingDiscountPolicyJson: '{}',
rateLimitPolicyJson: '{"rules":[]}',
quotaPolicyJson: '{}',
metadataJson: '{}',
status: 'active',
};
}
function userGroupToForm(group: UserGroup): UserGroupForm {
return {
groupKey: group.groupKey,
name: group.name,
description: group.description ?? '',
source: group.source,
priority: String(group.priority),
rechargeDiscountPolicyJson: stringifyJson(group.rechargeDiscountPolicy),
billingDiscountPolicyJson: stringifyJson(group.billingDiscountPolicy),
rateLimitPolicyJson: stringifyJson(group.rateLimitPolicy),
quotaPolicyJson: stringifyJson(group.quotaPolicy),
metadataJson: stringifyJson(group.metadata),
status: group.status,
};
}
function formToUserGroupPayload(form: UserGroupForm): UserGroupUpsertRequest {
return {
groupKey: form.groupKey.trim(),
name: form.name.trim(),
description: form.description.trim() || undefined,
source: form.source,
priority: Number(form.priority) || 100,
rechargeDiscountPolicy: parseJsonObject(form.rechargeDiscountPolicyJson, '充值折扣策略 JSON'),
billingDiscountPolicy: parseJsonObject(form.billingDiscountPolicyJson, '计费折扣策略 JSON'),
rateLimitPolicy: parseJsonObject(form.rateLimitPolicyJson, '限流策略 JSON'),
quotaPolicy: parseJsonObject(form.quotaPolicyJson, '额度策略 JSON'),
metadata: parseJsonObject(form.metadataJson, '元数据 JSON'),
status: form.status,
};
}
function tenantName(tenants: GatewayTenant[], tenantId?: string, fallback?: string) {
if (!tenantId) return fallback || '未设置';
return tenants.find((tenant) => tenant.id === tenantId)?.name ?? fallback ?? tenantId;
}
function groupName(groups: UserGroup[], groupId?: string) {
if (!groupId) return '未设置';
return groups.find((group) => group.id === groupId)?.name ?? groupId;
}
function roleValue(roles?: string[]) {
if (roles?.includes('manager')) return 'manager';
if (roles?.includes('admin')) return 'admin';
if (roles?.includes('operator')) return 'operator';
if (roles?.includes('creator')) return 'creator';
return 'user';
}
function roleLabel(roles?: string[]) {
return roleOptions.find((role) => role.value === roleValue(roles))?.label ?? '普通用户';
}
function discountSummary(group: UserGroup) {
const billing = group.billingDiscountPolicy?.discountFactor ?? group.billingDiscountPolicy?.factor;
const recharge = group.rechargeDiscountPolicy?.discountFactor ?? group.rechargeDiscountPolicy?.factor;
const parts = [];
if (billing) parts.push(`计费 ${billing}`);
if (recharge) parts.push(`充值 ${recharge}`);
return parts.join(' / ') || '未设置';
}
function policyKeys(value?: Record<string, unknown>) {
if (!value) return [];
return Object.keys(value).slice(0, 3);
}
function stringifyJson(value: unknown) {
if (!value || (typeof value === 'object' && Object.keys(value as Record<string, unknown>).length === 0)) return '{}';
return JSON.stringify(value, null, 2);
}
function parseJsonObject(value: string, label: string): Record<string, unknown> | undefined {
const text = value.trim();
if (!text || text === '{}') return undefined;
const parsed = JSON.parse(text) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`${label} 必须是 JSON 对象`);
}
return parsed as Record<string, unknown>;
}