Files
edgeKey/modules/inventory/service.ts
2026-05-13 11:56:18 +08:00

252 lines
7.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getContext } from "telefunc";
import type { PrismaClient } from "../../generated/prisma/client";
import { badRequestError } from "../../lib/app-error";
import { getAdminContext, logAdminOperation } from "../auth/service";
import { parseCardLines } from "./importer";
import { countCardStats, createCardRecord, createManyCards, deleteCardById, deleteUnusedCardsByProduct, listCardRecords, listCardRecordsPaged, findExistingCardContents } from "./repository";
function getInventoryContext() {
return getContext<{ prisma: PrismaClient }>();
}
function previewCard(content: string) {
if (content.length <= 8) {
return content;
}
return `${content.slice(0, 4)}****${content.slice(-4)}`;
}
export async function getInventoryOverview(prisma?: PrismaClient) {
const client = prisma ?? getInventoryContext().prisma;
const stats = await countCardStats(client);
const summary = {
total: 0,
available: 0,
sold: 0,
};
for (const item of stats) {
summary.total += item._count._all;
if (item.status === "UNUSED") summary.available += item._count._all;
if (item.status === "SOLD") summary.sold += item._count._all;
}
return summary;
}
export async function getAdminCards(prisma?: PrismaClient) {
const client = prisma ?? getInventoryContext().prisma;
const cards = await listCardRecords(client);
return cards.map((item) => ({
id: item.id,
productId: item.productId,
productName: item.product.name,
status: item.status,
batchNo: item.batchNo,
orderId: item.orderId,
soldAt: item.soldAt ? item.soldAt.toISOString() : null,
createdAt: item.createdAt.toISOString(),
contentPreview: previewCard(item.content),
}));
}
export async function createCard(input: { productId: number; content: string; batchNo?: string; force?: boolean }) {
const adminContext = getAdminContext();
const { prisma } = adminContext;
const adminId = Number(adminContext.session?.user?.id);
const content = input.content.trim();
if (!content) {
throw badRequestError("卡密内容不能为空", "CARD_CONTENT_REQUIRED");
}
// 验证商品是否存在且为自动发货卡密商品
const product = await prisma.product.findUnique({ where: { id: input.productId } });
if (!product) {
throw badRequestError('请选择要导入卡密的商品', "PRODUCT_NOT_FOUND");
}
if (product.deliveryType !== "CARD_AUTO") {
throw badRequestError("只有自动发货卡密商品可以新增卡密库存", "PRODUCT_DELIVERY_TYPE_NOT_CARD_AUTO");
}
// 检查数据库中是否存在重复卡密
const existingContents = await findExistingCardContents(prisma, input.productId, [content]);
if (existingContents.length > 0 && !input.force) {
return {
requiresConfirmation: true,
message: existingContents.length === 1
? `该卡密「${existingContents[0]}」已存在于系统中,是否继续添加?`
: `该卡密「${existingContents[0]}」已存在于系统中共有 ${existingContents.length} 条记录,是否继续添加?`,
existingContent: existingContents,
};
}
const card = await createCardRecord(prisma, {
productId: input.productId,
content,
batchNo: input.batchNo?.trim() || null,
});
await logAdminOperation(
{
action: "CREATE_CARD",
targetType: "Card",
targetId: String(card.id),
detail: `productId=${card.productId}`,
},
{
prisma,
adminId,
},
);
return {
id: card.id,
productId: card.productId,
productName: card.product.name,
status: card.status,
batchNo: card.batchNo,
orderId: card.orderId,
soldAt: null,
createdAt: card.createdAt.toISOString(),
contentPreview: previewCard(card.content),
};
}
export async function importCards(input: { productId: number; lines: string; batchNo?: string; skipInputDedup?: boolean; force?: boolean }) {
const adminContext = getAdminContext();
const { prisma } = adminContext;
const adminId = Number(adminContext.session?.user?.id);
const { allItems, uniqueItems, removedCount } = parseCardLines(input.lines);
if (!allItems.length) {
throw badRequestError("没有可导入的卡密内容", "CARD_IMPORT_EMPTY");
}
// 检测输入编辑框是否有重复项目skipInputDedup=true 时跳过)
if (removedCount > 0 && !input.skipInputDedup) {
return {
requiresConfirmation: true,
type: "input_duplicates" as const,
message: "您当前输入的卡密重复,是否删除重复卡密?",
removedCount,
items: uniqueItems,
};
}
const items = input.skipInputDedup ? allItems : uniqueItems;
// 验证商品是否存在且为自动发货卡密商品
const product = await prisma.product.findUnique({ where: { id: input.productId } });
if (!product) {
throw badRequestError('请选择要导入卡密的商品', "PRODUCT_NOT_FOUND");
}
if (product.deliveryType !== "CARD_AUTO") {
throw badRequestError("只有自动发货卡密商品可以批量导入卡密库存", "PRODUCT_DELIVERY_TYPE_NOT_CARD_AUTO");
}
// 检测数据库是否有重复卡密force=true 时跳过)
const existingContents = await findExistingCardContents(prisma, input.productId, items);
if (existingContents.length > 0 && !input.force) {
return {
requiresConfirmation: true,
type: "db_duplicates" as const,
message: existingContents.length === 1
? `该卡密「${existingContents[0]}」已存在于系统中,是否继续添加?`
: `该卡密「${existingContents[0]}」已存在于系统中共有 ${existingContents.length} 条记录,是否继续添加?`,
existingContents,
items,
};
}
await createManyCards(
prisma,
items.map((content) => ({
productId: input.productId,
content,
batchNo: input.batchNo?.trim() || null,
})),
);
await logAdminOperation(
{
action: "IMPORT_CARDS",
targetType: "Card",
targetId: String(input.productId),
detail: `count=${items.length}`,
},
{
prisma,
adminId,
},
);
return {
count: items.length,
};
}
export async function deleteUnusedCards(input: { productId: number }) {
const adminContext = getAdminContext();
const { prisma } = adminContext;
const adminId = Number(adminContext.session?.user?.id);
const result = await deleteUnusedCardsByProduct(prisma, input.productId);
await logAdminOperation(
{
action: "DELETE_UNUSED_CARDS",
targetType: "Card",
targetId: String(input.productId),
detail: `count=${result.count}`,
},
{
prisma,
adminId,
},
);
return {
count: result.count,
};
}
export async function deleteCard(input: { id: number }) {
const adminContext = getAdminContext();
const { prisma } = adminContext;
const adminId = Number(adminContext.session?.user?.id);
const result = await deleteCardById(prisma, input.id);
if (result.count === 0) throw badRequestError("卡密不存在或已售出,无法删除", "CARD_DELETE_FAILED");
await logAdminOperation({ action: "DELETE_CARD", targetType: "Card", targetId: String(input.id), detail: "" }, { prisma, adminId });
return { id: input.id };
}
export async function getAdminCardsPaged(params: {
productId?: number;
batchNo?: string;
status?: string;
startDate?: string;
endDate?: string;
page: number;
pageSize: number;
}) {
const { prisma } = getAdminContext();
const [cards, total] = await listCardRecordsPaged(prisma, params);
return {
total,
items: cards.map((item) => ({
id: item.id,
productId: item.productId,
productName: item.product.name,
status: item.status,
batchNo: item.batchNo,
orderId: item.orderId,
soldAt: item.soldAt ? item.soldAt.toISOString() : null,
createdAt: item.createdAt.toISOString(),
contentPreview: previewCard(item.content),
})),
};
}