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