|
|
import { prisma } from "@/lib/prisma";
|
|
|
import { ValidationError } from "@/modules/quote/types";
|
|
|
|
|
|
export type BusinessCustomerUserDto = {
|
|
|
customer_id: string;
|
|
|
business_customer_id: string;
|
|
|
external_user_id: string;
|
|
|
account: string;
|
|
|
display_name: string | null;
|
|
|
email: string | null;
|
|
|
mobile: string | null;
|
|
|
main_external_user_id: string | null;
|
|
|
status: "active" | "inactive";
|
|
|
created_at: string;
|
|
|
updated_at: string;
|
|
|
};
|
|
|
|
|
|
export type ResolvedBusinessCustomerByAccount = {
|
|
|
customer_id: string;
|
|
|
business_customer_id: string;
|
|
|
external_code: string | null;
|
|
|
name: string;
|
|
|
account: string;
|
|
|
};
|
|
|
|
|
|
function serializeUser(row: {
|
|
|
customerId: string;
|
|
|
businessCustomerId: string;
|
|
|
externalUserId: string;
|
|
|
account: string;
|
|
|
displayName: string | null;
|
|
|
email: string | null;
|
|
|
mobile: string | null;
|
|
|
mainExternalUserId: string | null;
|
|
|
status: string;
|
|
|
createdAt: Date;
|
|
|
updatedAt: Date;
|
|
|
}): BusinessCustomerUserDto {
|
|
|
return {
|
|
|
customer_id: row.customerId,
|
|
|
business_customer_id: row.businessCustomerId,
|
|
|
external_user_id: row.externalUserId,
|
|
|
account: row.account,
|
|
|
display_name: row.displayName,
|
|
|
email: row.email,
|
|
|
mobile: row.mobile,
|
|
|
main_external_user_id: row.mainExternalUserId,
|
|
|
status: row.status === "active" ? "active" : "inactive",
|
|
|
created_at: row.createdAt.toISOString(),
|
|
|
updated_at: row.updatedAt.toISOString(),
|
|
|
};
|
|
|
}
|
|
|
|
|
|
export async function listUsersByBusinessCustomer(
|
|
|
customerId: string,
|
|
|
businessCustomerId: string,
|
|
|
): Promise<BusinessCustomerUserDto[]> {
|
|
|
const rows = await prisma.businessCustomerUser.findMany({
|
|
|
where: {
|
|
|
customerId,
|
|
|
businessCustomerId,
|
|
|
isDeleted: false,
|
|
|
},
|
|
|
orderBy: [{ status: "asc" }, { account: "asc" }],
|
|
|
});
|
|
|
return rows.map(serializeUser);
|
|
|
}
|
|
|
|
|
|
export async function listUsersByTenant(
|
|
|
customerId: string,
|
|
|
keyword?: string,
|
|
|
): Promise<
|
|
|
Array<
|
|
|
BusinessCustomerUserDto & {
|
|
|
business_customer_code: string | null;
|
|
|
business_customer_name: string;
|
|
|
}
|
|
|
>
|
|
|
> {
|
|
|
const kw = keyword?.trim();
|
|
|
const rows = await prisma.businessCustomerUser.findMany({
|
|
|
where: {
|
|
|
customerId,
|
|
|
isDeleted: false,
|
|
|
...(kw
|
|
|
? {
|
|
|
OR: [
|
|
|
{ account: { contains: kw } },
|
|
|
{ displayName: { contains: kw } },
|
|
|
{ businessCustomer: { externalCode: { contains: kw } } },
|
|
|
{ businessCustomer: { name: { contains: kw } } },
|
|
|
],
|
|
|
}
|
|
|
: {}),
|
|
|
},
|
|
|
include: {
|
|
|
businessCustomer: {
|
|
|
select: { externalCode: true, name: true },
|
|
|
},
|
|
|
},
|
|
|
orderBy: [{ status: "asc" }, { account: "asc" }],
|
|
|
take: 2000,
|
|
|
});
|
|
|
return rows.map((row) => ({
|
|
|
...serializeUser(row),
|
|
|
business_customer_code: row.businessCustomer.externalCode,
|
|
|
business_customer_name: row.businessCustomer.name,
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 按登录账号反查业务客户(仅 active)。
|
|
|
* 账号大小写不敏感;同租户同账号多条时取最近更新。
|
|
|
*/
|
|
|
export async function resolveBusinessCustomerByAccount(
|
|
|
customerId: string,
|
|
|
accountRaw: string,
|
|
|
): Promise<ResolvedBusinessCustomerByAccount | null> {
|
|
|
const account = accountRaw.trim();
|
|
|
if (!customerId.trim() || !account) {
|
|
|
return null;
|
|
|
}
|
|
|
const row = await prisma.businessCustomerUser.findFirst({
|
|
|
where: {
|
|
|
customerId,
|
|
|
isDeleted: false,
|
|
|
status: "active",
|
|
|
account, // MySQL utf8mb4_unicode_ci 大小写不敏感
|
|
|
businessCustomer: {
|
|
|
isDeleted: false,
|
|
|
status: "active",
|
|
|
},
|
|
|
},
|
|
|
orderBy: { updatedAt: "desc" },
|
|
|
include: {
|
|
|
businessCustomer: {
|
|
|
select: {
|
|
|
businessCustomerId: true,
|
|
|
externalCode: true,
|
|
|
name: true,
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
});
|
|
|
if (!row) {
|
|
|
return null;
|
|
|
}
|
|
|
return {
|
|
|
customer_id: row.customerId,
|
|
|
business_customer_id: row.businessCustomer.businessCustomerId,
|
|
|
external_code: row.businessCustomer.externalCode,
|
|
|
name: row.businessCustomer.name,
|
|
|
account: row.account,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/** 无 BC 时用账号补全;账号无效则抛 ValidationError */
|
|
|
export async function resolveBusinessCustomerIdForQuote(input: {
|
|
|
customerId: string;
|
|
|
businessCustomerId?: string | null;
|
|
|
businessUserAccount?: string | null;
|
|
|
}): Promise<string | undefined> {
|
|
|
const explicit = input.businessCustomerId?.trim() || undefined;
|
|
|
if (explicit) {
|
|
|
return explicit;
|
|
|
}
|
|
|
const account = input.businessUserAccount?.trim() || "";
|
|
|
if (!account) {
|
|
|
return undefined;
|
|
|
}
|
|
|
const resolved = await resolveBusinessCustomerByAccount(
|
|
|
input.customerId,
|
|
|
account,
|
|
|
);
|
|
|
if (!resolved) {
|
|
|
throw new ValidationError(
|
|
|
`未识别到业务客户账号「${account}」,请确认账号已同步或显式传入业务客户`,
|
|
|
);
|
|
|
}
|
|
|
return resolved.business_customer_id;
|
|
|
}
|
|
|
|
|
|
export type UpsertBusinessCustomerUserInput = {
|
|
|
businessCustomerId: string;
|
|
|
externalUserId: string;
|
|
|
account: string;
|
|
|
displayName?: string | null;
|
|
|
email?: string | null;
|
|
|
mobile?: string | null;
|
|
|
mainExternalUserId?: string | null;
|
|
|
status?: "active" | "inactive";
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 批量 upsert 业务客户登录账号(按 externalUserId 唯一)。
|
|
|
* 若业务客户不存在则跳过该行并计入 skipped。
|
|
|
*/
|
|
|
export async function upsertBusinessCustomerUsers(
|
|
|
customerId: string,
|
|
|
rows: UpsertBusinessCustomerUserInput[],
|
|
|
): Promise<{ created: number; updated: number; skipped: number }> {
|
|
|
let created = 0;
|
|
|
let updated = 0;
|
|
|
let skipped = 0;
|
|
|
const tenantId = customerId.trim();
|
|
|
if (!tenantId) {
|
|
|
throw new ValidationError("租户标识不能为空");
|
|
|
}
|
|
|
|
|
|
for (const row of rows) {
|
|
|
const businessCustomerId = row.businessCustomerId?.trim() || "";
|
|
|
const externalUserId = row.externalUserId?.trim() || "";
|
|
|
const account = row.account?.trim() || "";
|
|
|
if (!businessCustomerId || !externalUserId || !account) {
|
|
|
skipped += 1;
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
const bc = await prisma.businessCustomer.findFirst({
|
|
|
where: {
|
|
|
customerId: tenantId,
|
|
|
businessCustomerId,
|
|
|
isDeleted: false,
|
|
|
},
|
|
|
select: { businessCustomerId: true },
|
|
|
});
|
|
|
if (!bc) {
|
|
|
skipped += 1;
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
const existing = await prisma.businessCustomerUser.findFirst({
|
|
|
where: {
|
|
|
customerId: tenantId,
|
|
|
externalUserId,
|
|
|
},
|
|
|
});
|
|
|
|
|
|
const data = {
|
|
|
businessCustomerId,
|
|
|
account,
|
|
|
displayName: row.displayName?.trim() || null,
|
|
|
email: row.email?.trim() || null,
|
|
|
mobile: row.mobile?.trim() || null,
|
|
|
mainExternalUserId: row.mainExternalUserId?.trim() || null,
|
|
|
status: row.status === "inactive" ? "inactive" : "active",
|
|
|
isDeleted: false,
|
|
|
};
|
|
|
|
|
|
if (existing) {
|
|
|
await prisma.businessCustomerUser.update({
|
|
|
where: { id: existing.id },
|
|
|
data,
|
|
|
});
|
|
|
updated += 1;
|
|
|
} else {
|
|
|
await prisma.businessCustomerUser.create({
|
|
|
data: {
|
|
|
customerId: tenantId,
|
|
|
externalUserId,
|
|
|
...data,
|
|
|
},
|
|
|
});
|
|
|
created += 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return { created, updated, skipped };
|
|
|
}
|
|
|
|