import { prisma } from "@/lib/prisma"; import type { MarkupConfig } from "@prisma/client"; import { listAllCustomerIds } from "@/modules/auth/service-token"; import { getBusinessCustomer, listBusinessCustomers, } from "@/modules/customer/business-customer-service"; import type { ParsedMarkupInput } from "@/modules/pricing/markup-validation"; export type MarkupConfigDto = { customer_id: string; business_customer_id: string | null; /** 客户代码(Bas_Customer.F_CustomerCode / external_code) */ business_customer_code?: string | null; /** 客户名称(简称优先) */ business_customer_name?: string | null; markup_type: "percent" | "fixed"; markup_percent: number; markup_fixed_amount: number | null; operator_id: string; remark: string | null; updated_at: string; }; export function serializeMarkupConfig( config: MarkupConfig, extras?: { business_customer_code?: string | null; business_customer_name?: string | null; }, ): MarkupConfigDto { return { customer_id: config.customerId, business_customer_id: config.businessCustomerId, business_customer_code: extras?.business_customer_code ?? null, business_customer_name: extras?.business_customer_name ?? null, markup_type: config.markupType === "fixed" ? "fixed" : "percent", markup_percent: Number(config.markupPercent), markup_fixed_amount: config.markupFixedAmount === null || config.markupFixedAmount === undefined ? null : Number(config.markupFixedAmount), operator_id: config.operatorId, remark: config.remark, updated_at: config.updatedAt.toISOString(), }; } export function defaultMarkupDto( customerId: string, businessCustomerId?: string | null, extras?: { business_customer_code?: string | null; business_customer_name?: string | null; }, ): MarkupConfigDto { return { customer_id: customerId, business_customer_id: businessCustomerId ?? null, business_customer_code: extras?.business_customer_code ?? null, business_customer_name: extras?.business_customer_name ?? null, markup_type: "percent", markup_percent: 0, markup_fixed_amount: null, operator_id: "", remark: null, updated_at: new Date(0).toISOString(), }; } export async function upsertMarkupConfig( customerId: string, businessCustomerId: string | null, input: ParsedMarkupInput, operatorId: string, remark: string | null, ): Promise { if (!businessCustomerId) { throw new Error("仅支持业务客户加价,租户不加价"); } const existing = await prisma.markupConfig.findFirst({ where: { customerId, businessCustomerId, isDeleted: false }, }); const config = existing ? await prisma.markupConfig.update({ where: { id: existing.id }, data: { markupType: input.markupType, markupPercent: input.markupPercent, markupFixedAmount: input.markupFixedAmount, operatorId, remark, isDeleted: false, }, }) : await prisma.markupConfig.create({ data: { customerId, businessCustomerId, markupType: input.markupType, markupPercent: input.markupPercent, markupFixedAmount: input.markupFixedAmount, operatorId, remark, }, }); // 为何这样改:PUT 响应会被前端整行替换;缺 name/code 时列表回退成 UUID const businessCustomer = await getBusinessCustomer( customerId, businessCustomerId, ); return serializeMarkupConfig(config, { business_customer_code: businessCustomer?.external_code ?? null, business_customer_name: businessCustomer?.name ?? null, }); } function keywordSearchVariants(raw: string): string[] { const k = raw.trim().toLowerCase(); if (!k) return []; const variants = new Set([k]); // 客户代码常把 0/O、1/l 混输(截图 SPO vs SP012) variants.add(k.replace(/o/g, "0")); variants.add(k.replace(/0/g, "o")); variants.add(k.replace(/l/g, "1")); variants.add(k.replace(/1/g, "l")); return [...variants]; } function textMatchesKeyword(haystack: string, variants: string[]): boolean { const h = haystack.trim().toLowerCase(); if (!h) return false; return variants.some((v) => h.includes(v)); } export async function listAdminMarkupConfigs(options: { page: number; size: number; keyword?: string; }): Promise<{ list: MarkupConfigDto[]; total: number; page: number; size: number }> { const variants = keywordSearchVariants(options.keyword ?? ""); const customerIds = await listAllCustomerIds(); if (customerIds.length === 0) { return { list: [], total: 0, page: options.page, size: options.size, }; } const configs = await prisma.markupConfig.findMany({ where: { customerId: { in: customerIds }, isDeleted: false }, }); const configMap = new Map( configs.map((c) => [ `${c.customerId}::${c.businessCustomerId ?? "__default__"}`, c, ]), ); // 登录账号命中 → 反查业务客户(支持搜 STARPOST) const accountHitKeys = new Set(); if (variants.length > 0) { const accountRows = await prisma.businessCustomerUser.findMany({ where: { isDeleted: false, customerId: { in: customerIds }, OR: variants.flatMap((v) => [ { account: { contains: v } }, { displayName: { contains: v } }, ]), }, select: { customerId: true, businessCustomerId: true }, }); for (const row of accountRows) { accountHitKeys.add(`${row.customerId}::${row.businessCustomerId}`); } } const merged: MarkupConfigDto[] = []; for (const customerId of customerIds) { const businessCustomers = await listBusinessCustomers(customerId); for (const businessCustomer of businessCustomers) { const code = (businessCustomer.external_code ?? "").trim(); const name = (businessCustomer.name ?? "").trim(); if (variants.length > 0) { const key = `${customerId}::${businessCustomer.business_customer_id}`; const hit = accountHitKeys.has(key) || textMatchesKeyword(customerId, variants) || textMatchesKeyword(businessCustomer.business_customer_id, variants) || textMatchesKeyword(name, variants) || textMatchesKeyword(code, variants); if (!hit) continue; } const extras = { business_customer_code: code || null, business_customer_name: name || null, }; const existing = configMap.get( `${customerId}::${businessCustomer.business_customer_id}`, ); merged.push( existing ? serializeMarkupConfig(existing, extras) : defaultMarkupDto( customerId, businessCustomer.business_customer_id, extras, ), ); } } // 有关键字时:代码/名称更靠前,便于精准命中 if (variants.length > 0) { const primary = variants[0]!; merged.sort((a, b) => { const score = (row: MarkupConfigDto): number => { const code = (row.business_customer_code ?? "").toLowerCase(); const name = (row.business_customer_name ?? "").toLowerCase(); if (code === primary || name === primary) return 0; if (code.startsWith(primary) || name.startsWith(primary)) return 1; if (code.includes(primary) || name.includes(primary)) return 2; return 3; }; const d = score(a) - score(b); if (d !== 0) return d; return (a.business_customer_code ?? "").localeCompare( b.business_customer_code ?? "", ); }); } const total = merged.length; const start = (options.page - 1) * options.size; const list = merged.slice(start, start + options.size); return { list, total, page: options.page, size: options.size, }; }