Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions src/actions/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,38 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
/**
* 获取所有可用的供应商分组标签(用于用户表单中的下拉建议)
*/
export async function getAvailableProviderGroups(): Promise<string[]> {
/**
* 获取所有可用的供应商分组列表
* @param userId - 可选的用户ID,用于过滤用户可用的分组
* @returns 供应商分组列表
*/
export async function getAvailableProviderGroups(userId?: number): Promise<string[]> {
try {
const { getDistinctProviderGroups } = await import("@/repository/provider");
return await getDistinctProviderGroups();
const allGroups = await getDistinctProviderGroups();

// 如果没有提供 userId,返回所有分组(向后兼容)
if (!userId) {
return allGroups;
}

// 查询用户配置的 providerGroup
const { findUserById } = await import("@/repository/user");
const user = await findUserById(userId);

if (!user || !user.providerGroup) {
// 用户未配置 providerGroup,返回所有分组
return allGroups;
}

// 解析用户的 providerGroup(逗号分隔)
const userGroups = user.providerGroup
.split(",")
.map((g) => g.trim())
.filter(Boolean);

// 过滤:只返回用户配置的分组
return allGroups.filter((group) => userGroups.includes(group));
} catch (error) {
logger.error("获取供应商分组失败:", error);
return [];
Expand Down
76 changes: 76 additions & 0 deletions src/actions/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
findKeyList,
findKeysWithStatistics,
findKeyUsageToday,
updateKey,
} from "@/repository/key";
import { createUser, deleteUser, findUserById, findUserList, updateUser } from "@/repository/user";
import type { UserDisplay } from "@/types/user";
Expand Down Expand Up @@ -157,6 +158,7 @@ export async function getUsers(): Promise<UserDisplay[]> {
limitMonthlyUsd: key.limitMonthlyUsd,
limitTotalUsd: key.limitTotalUsd,
limitConcurrentSessions: key.limitConcurrentSessions || 0,
providerGroup: key.providerGroup,
};
}),
};
Expand Down Expand Up @@ -419,6 +421,9 @@ export async function editUser(
}
}

// 在更新前获取旧用户数据(用于级联更新判断)
const oldUserForCascade = data.providerGroup !== undefined ? await findUserById(userId) : null;

// Update user with validated data
await updateUser(userId, {
name: validatedData.name,
Expand All @@ -436,6 +441,77 @@ export async function editUser(
expiresAt: data.expiresAt,
});

// 级联更新 KEY 的 providerGroup(仅针对减少场景)
if (oldUserForCascade && data.providerGroup !== undefined) {
// 只有在 providerGroup 真正变化时才级联更新
if (oldUserForCascade.providerGroup !== data.providerGroup) {
const oldUserGroups = oldUserForCascade.providerGroup
? oldUserForCascade.providerGroup.split(",").map((g) => g.trim()).filter(Boolean)
: [];

const newUserGroups = data.providerGroup
? data.providerGroup.split(",").map((g) => g.trim()).filter(Boolean)
: [];

// 计算被移除的分组
const removedGroups = oldUserGroups.filter((g) => !newUserGroups.includes(g));

// 如果没有移除分组(只新增),直接跳过
if (removedGroups.length === 0) {
logger.debug(`用户 ${userId} 的 providerGroup 只新增分组,无需级联更新 KEY`);
} else {
// 有移除分组,需要级联更新 KEY
logger.info(
`用户 ${userId} 移除了供应商分组: ${removedGroups.join(",")},开始级联更新 KEY`
);

// 获取该用户的所有 KEY
const userKeys = await findKeyList(userId);

for (const key of userKeys) {
if (!key.providerGroup) {
// KEY 未设置 providerGroup,继承用户配置,无需更新
continue;
}

// 解析 KEY 的分组列表
const keyGroups = key.providerGroup
.split(",")
.map((g) => g.trim())
.filter(Boolean);

// 检查 KEY 是否包含被移除的分组
const hasRemovedGroups = keyGroups.some((g) => removedGroups.includes(g));
if (!hasRemovedGroups) {
// KEY 不包含被移除的分组,无需更新
continue;
}

// 过滤:只保留在用户新范围内的分组
const filteredGroups = keyGroups.filter((g) => newUserGroups.includes(g));

// 计算新值
const newKeyProviderGroup = filteredGroups.length > 0 ? filteredGroups.join(",") : null;

// 如果值发生变化,更新 KEY
if (newKeyProviderGroup !== key.providerGroup) {
await updateKey(key.id, {
provider_group: newKeyProviderGroup,
});

logger.info(`级联更新 KEY ${key.id} 的 providerGroup`, {
keyName: key.name,
oldValue: key.providerGroup,
newValue: newKeyProviderGroup,
removedGroups: removedGroups.join(","),
reason: "用户 providerGroup 减少",
});
}
}
}
}
}

revalidatePath("/dashboard");
return { ok: true };
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ export function AddKeyForm({ userId, user, onSuccess }: AddKeyFormProps) {

// Load provider group suggestions
useEffect(() => {
getAvailableProviderGroups().then(setProviderGroupSuggestions);
}, []);
if (user?.id) {
getAvailableProviderGroups(user.id).then(setProviderGroupSuggestions);
} else {
getAvailableProviderGroups().then(setProviderGroupSuggestions);
}
}, [user?.id]);
Comment on lines 38 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This useEffect hook can be simplified. The getAvailableProviderGroups function already handles an optional userId. You can pass user?.id directly without the if/else block.

  useEffect(() => {
    getAvailableProviderGroups(user?.id).then(setProviderGroupSuggestions);
  }, [user?.id]);


const form = useZodForm({
schema: KeyFormSchema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,12 @@ export function EditKeyForm({ keyData, user, onSuccess }: EditKeyFormProps) {

// Load provider group suggestions
useEffect(() => {
getAvailableProviderGroups().then(setProviderGroupSuggestions);
}, []);
if (user?.id) {
getAvailableProviderGroups(user.id).then(setProviderGroupSuggestions);
} else {
getAvailableProviderGroups().then(setProviderGroupSuggestions);
}
}, [user?.id]);

const formatExpiresAt = (expiresAt: string) => {
if (!expiresAt || expiresAt === "永不过期") return "";
Expand Down
2 changes: 2 additions & 0 deletions src/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export interface UserKeyDisplay {
limitMonthlyUsd: number | null; // 月消费上限(美元)
limitTotalUsd?: number | null; // 总消费上限(美元)
limitConcurrentSessions: number; // 并发 Session 上限
// Provider group override (null = inherit from user)
providerGroup?: string | null;
}

/**
Expand Down
Loading