You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chajia/modules/customer/business-customer-user-serv...

271 lines
6.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 };
}