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