459 lines
10 KiB
TypeScript
459 lines
10 KiB
TypeScript
import { UrlMeta, UserRole } from "@prisma/client";
|
|
|
|
import { prisma } from "@/lib/db";
|
|
|
|
import { EXPIRATION_ENUMS } from "../enums";
|
|
import { getStartDate } from "../utils";
|
|
|
|
export interface ShortUrlFormData {
|
|
id?: string;
|
|
userId: string;
|
|
userName: string;
|
|
target: string;
|
|
url: string;
|
|
prefix: string;
|
|
visible: number;
|
|
active: number;
|
|
expiration: string;
|
|
password: string;
|
|
createdAt?: Date;
|
|
updatedAt?: Date;
|
|
}
|
|
|
|
export interface UserShortUrlInfo extends ShortUrlFormData {
|
|
// meta: Omit<UrlMeta, "id">;
|
|
meta?: UrlMeta;
|
|
}
|
|
|
|
export async function getUserShortUrls(
|
|
userId: string,
|
|
active: number = 1,
|
|
page: number,
|
|
size: number,
|
|
role: UserRole = "USER",
|
|
userName: string = "",
|
|
url: string = "",
|
|
target: string = "",
|
|
) {
|
|
let option: any =
|
|
role === "USER"
|
|
? {
|
|
userId,
|
|
}
|
|
: {};
|
|
|
|
if (userName) {
|
|
option.userName = {
|
|
contains: userName,
|
|
};
|
|
}
|
|
if (url) {
|
|
option.url = {
|
|
contains: url,
|
|
};
|
|
}
|
|
if (target) {
|
|
option.target = {
|
|
contains: target,
|
|
};
|
|
}
|
|
|
|
const [total, list] = await prisma.$transaction([
|
|
prisma.userUrl.count({
|
|
where: option,
|
|
}),
|
|
prisma.userUrl.findMany({
|
|
where: option,
|
|
skip: (page - 1) * size,
|
|
take: size,
|
|
orderBy: {
|
|
updatedAt: "desc",
|
|
},
|
|
}),
|
|
]);
|
|
return {
|
|
total,
|
|
list,
|
|
};
|
|
}
|
|
|
|
export async function getUserShortUrlCount(
|
|
userId: string,
|
|
active: number = 1,
|
|
role: UserRole = "USER",
|
|
) {
|
|
try {
|
|
// Start of last month from now
|
|
// const end = new Date();
|
|
// const start = new Date(
|
|
// end.getFullYear(),
|
|
// end.getMonth() - 1,
|
|
// end.getDate(),
|
|
// end.getHours(),
|
|
// end.getMinutes(),
|
|
// end.getSeconds(),
|
|
// );
|
|
|
|
// Start of current month
|
|
const now = new Date();
|
|
const start = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
const end = new Date(
|
|
now.getFullYear(),
|
|
now.getMonth() + 1,
|
|
0,
|
|
23,
|
|
59,
|
|
59,
|
|
999,
|
|
);
|
|
|
|
const [total, month_total] = await prisma.$transaction([
|
|
prisma.userUrl.count({
|
|
where: role === "USER" ? { userId } : {},
|
|
}),
|
|
prisma.userUrl.count({
|
|
where:
|
|
role === "USER"
|
|
? { userId, createdAt: { gte: start, lte: end } }
|
|
: { createdAt: { gte: start, lte: end } },
|
|
}),
|
|
]);
|
|
return { total, month_total };
|
|
} catch (error) {
|
|
return { total: -1, month_total: -1 };
|
|
}
|
|
}
|
|
|
|
export async function getUserShortLinksByIds(ids: string[], userId: string) {
|
|
try {
|
|
return await prisma.userUrl.findMany({
|
|
where: {
|
|
id: { in: ids },
|
|
userId,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function getUrlClicksByIds(
|
|
ids: string[],
|
|
userId: string,
|
|
role: UserRole,
|
|
): Promise<Record<string, number>> {
|
|
if (ids.length === 0) return {};
|
|
|
|
try {
|
|
const clicksData = await prisma.urlMeta.groupBy({
|
|
by: ["urlId"],
|
|
where: {
|
|
urlId: { in: ids },
|
|
userUrl: role === "USER" ? { userId } : undefined,
|
|
},
|
|
_sum: { click: true },
|
|
});
|
|
|
|
const clicksMap: Record<string, number> = {};
|
|
ids.forEach((id) => (clicksMap[id] = 0)); // 初始化
|
|
clicksData.forEach((item) => {
|
|
clicksMap[item.urlId] = item._sum.click || 0;
|
|
});
|
|
|
|
return clicksMap;
|
|
} catch (error) {
|
|
console.error("Error fetching clicks:", error);
|
|
return Object.fromEntries(ids.map((id) => [id, 0]));
|
|
}
|
|
}
|
|
|
|
export async function getUrlStatus(userId: string, role: UserRole = "USER") {
|
|
try {
|
|
} catch (error) {
|
|
return { status: error };
|
|
}
|
|
}
|
|
|
|
export interface UrlStatusStats {
|
|
total: number;
|
|
actived: number; // 正常可用
|
|
disabled: number; // 已禁用
|
|
expired: number; // 已过期
|
|
passwordprotected: number; // 密码保护
|
|
}
|
|
|
|
function isValidExpirationValue(expiration: string): boolean {
|
|
return EXPIRATION_ENUMS.some((item) => item.value === expiration);
|
|
}
|
|
|
|
export async function getUrlStatusOptimized(
|
|
userId: string,
|
|
role: UserRole = "USER",
|
|
): Promise<UrlStatusStats | { status: any }> {
|
|
try {
|
|
const whereCondition = role === "USER" ? { userId } : {};
|
|
|
|
const urlRecords = await prisma.userUrl.findMany({
|
|
where: whereCondition,
|
|
select: {
|
|
id: true,
|
|
userId: true,
|
|
active: true,
|
|
expiration: true,
|
|
password: true,
|
|
createdAt: true,
|
|
updatedAt: true,
|
|
},
|
|
});
|
|
|
|
const now = Date.now();
|
|
const stats: UrlStatusStats = {
|
|
total: urlRecords.length,
|
|
actived: 0,
|
|
disabled: 0,
|
|
expired: 0,
|
|
passwordprotected: 0,
|
|
};
|
|
|
|
// 遍历记录并分类
|
|
urlRecords.forEach((record) => {
|
|
const updatedAt = new Date(
|
|
record.updatedAt || record.createdAt!,
|
|
).getTime();
|
|
|
|
// 判断是否过期
|
|
let isExpired = false;
|
|
if (
|
|
record.expiration !== "-1" &&
|
|
isValidExpirationValue(record.expiration)
|
|
) {
|
|
const expirationSeconds = Number(record.expiration);
|
|
const expirationMilliseconds = expirationSeconds * 1000;
|
|
const expirationTime = updatedAt + expirationMilliseconds;
|
|
isExpired = now > expirationTime;
|
|
}
|
|
|
|
const isDisabled = record.active === 0;
|
|
const hasPassword = Boolean(record.password && record.password.trim());
|
|
|
|
if (isExpired) {
|
|
stats.expired++;
|
|
} else if (isDisabled) {
|
|
stats.disabled++;
|
|
} else if (hasPassword) {
|
|
stats.passwordprotected++;
|
|
} else {
|
|
stats.actived++;
|
|
}
|
|
});
|
|
|
|
return stats;
|
|
} catch (error) {
|
|
console.error("Error getting URL status (optimized):", error);
|
|
return { status: error };
|
|
}
|
|
}
|
|
|
|
export async function createUserShortUrl(data: ShortUrlFormData) {
|
|
try {
|
|
const res = await prisma.userUrl.create({
|
|
data: {
|
|
userId: data.userId,
|
|
userName: data.userName || "Anonymous",
|
|
target: data.target,
|
|
url: data.url,
|
|
prefix: data.prefix,
|
|
visible: data.visible,
|
|
active: data.active,
|
|
expiration: data.expiration,
|
|
password: data.password,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
});
|
|
return { status: "success", data: res };
|
|
} catch (error) {
|
|
return { status: error };
|
|
}
|
|
}
|
|
|
|
export async function updateUserShortUrl(data: ShortUrlFormData) {
|
|
try {
|
|
const res = await prisma.userUrl.update({
|
|
where: {
|
|
id: data.id,
|
|
userId: data.userId,
|
|
},
|
|
data: {
|
|
target: data.target,
|
|
url: data.url,
|
|
visible: data.visible,
|
|
prefix: data.prefix,
|
|
// active: data.active,
|
|
expiration: data.expiration,
|
|
password: data.password,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
});
|
|
return { status: "success", data: res };
|
|
} catch (error) {
|
|
return { status: error };
|
|
}
|
|
}
|
|
|
|
export async function updateUserShortUrlActive(
|
|
userId: string,
|
|
id: string,
|
|
active: number = 1,
|
|
role: UserRole = "USER",
|
|
) {
|
|
try {
|
|
const option = role === "USER" ? { userId, id } : { id };
|
|
const res = await prisma.userUrl.update({
|
|
where: option,
|
|
data: {
|
|
active,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
});
|
|
return { status: "success", data: res };
|
|
} catch (error) {
|
|
return { status: error };
|
|
}
|
|
}
|
|
|
|
export async function updateUserShortUrlVisibility(
|
|
id: string,
|
|
visible: number,
|
|
) {
|
|
try {
|
|
const res = await prisma.userUrl.update({
|
|
where: {
|
|
id,
|
|
},
|
|
data: {
|
|
visible,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
});
|
|
return { status: "success", data: res };
|
|
} catch (error) {
|
|
return { status: error };
|
|
}
|
|
}
|
|
|
|
export async function deleteUserShortUrl(userId: string, urlId: string) {
|
|
return await prisma.userUrl.delete({
|
|
where: {
|
|
id: urlId,
|
|
userId,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function getUserUrlMetaInfo(
|
|
urlId: string,
|
|
dateRange: string = "",
|
|
) {
|
|
const startDate = getStartDate(dateRange);
|
|
return await prisma.urlMeta.findMany({
|
|
where: {
|
|
urlId,
|
|
...(startDate && {
|
|
createdAt: { gte: startDate },
|
|
}),
|
|
},
|
|
orderBy: { updatedAt: "asc" },
|
|
});
|
|
}
|
|
|
|
export async function getUrlBySuffix(suffix: string) {
|
|
return await prisma.userUrl.findFirst({
|
|
where: {
|
|
url: suffix,
|
|
},
|
|
select: {
|
|
id: true,
|
|
target: true,
|
|
active: true,
|
|
prefix: true,
|
|
expiration: true,
|
|
password: true,
|
|
updatedAt: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
// meta
|
|
export async function createUserShortUrlMeta(
|
|
data: Omit<UrlMeta, "id" | "createdAt" | "updatedAt">,
|
|
) {
|
|
try {
|
|
const meta = await findOrCreateUrlMeta(data);
|
|
return { status: "success", data: meta };
|
|
} catch (error) {
|
|
console.error("create meta error", error);
|
|
return { status: "error", message: error.message };
|
|
}
|
|
}
|
|
|
|
async function findOrCreateUrlMeta(data) {
|
|
const meta = await prisma.urlMeta.findFirst({
|
|
where: {
|
|
ip: data.ip,
|
|
urlId: data.urlId,
|
|
},
|
|
});
|
|
|
|
if (meta) {
|
|
return await incrementClick(meta.id);
|
|
} else {
|
|
return await prisma.urlMeta.create({ data });
|
|
}
|
|
}
|
|
|
|
async function incrementClick(id) {
|
|
return await prisma.urlMeta.update({
|
|
where: { id },
|
|
data: {
|
|
click: { increment: 1 },
|
|
updatedAt: new Date(), // Prisma will handle the ISO string conversion
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function getUrlMetaLiveLog(userId?: string) {
|
|
const whereClause = userId ? { userUrl: { userId } } : {};
|
|
|
|
const logs = await prisma.urlMeta.findMany({
|
|
take: 10,
|
|
where: whereClause,
|
|
orderBy: { updatedAt: "desc" },
|
|
select: {
|
|
ip: true,
|
|
click: true,
|
|
updatedAt: true,
|
|
createdAt: true,
|
|
city: true,
|
|
country: true,
|
|
os: true,
|
|
cpu: true,
|
|
engine: true,
|
|
userUrl: {
|
|
select: {
|
|
url: true,
|
|
target: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const formattedLogs = logs.map((log) => ({
|
|
...log,
|
|
slug: log.userUrl.url,
|
|
target: log.userUrl.target,
|
|
}));
|
|
|
|
return formattedLogs;
|
|
}
|