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.

248 lines
7.8 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 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<MarkupConfigDto> {
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<string>([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<string>();
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,
};
}