107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
import { z } from 'zod';
|
|
import type { CommissionImportCategory } from '@atlas/contracts';
|
|
import { ApiError } from '../common/api-error.js';
|
|
import { D } from './pricing-engine.js';
|
|
|
|
const sourceId = z
|
|
.union([
|
|
z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
z.string().regex(/^[1-9]\d{0,17}$/),
|
|
])
|
|
.transform(String);
|
|
const leaf = z
|
|
.object({
|
|
id: z.union([z.string(), z.number()]),
|
|
label: z.string().max(200),
|
|
value: z.string().max(50),
|
|
})
|
|
.strict();
|
|
const category = z
|
|
.object({
|
|
id: z.union([z.string(), z.number()]),
|
|
label: z.string().trim().min(1).max(200),
|
|
value: z.string().max(200),
|
|
cate_id: sourceId,
|
|
children: z.array(leaf).length(3),
|
|
})
|
|
.strict();
|
|
const root = category
|
|
.omit({ children: true })
|
|
.extend({ children: z.array(category).min(1).max(2000) });
|
|
const documentSchema = z
|
|
.object({
|
|
code: z.literal(1),
|
|
msg: z.string(),
|
|
data: z.array(root).min(1).max(100),
|
|
})
|
|
.strict();
|
|
const fail = (message: string): never => {
|
|
throw new ApiError(422, 'COMMISSION_IMPORT_INVALID', message);
|
|
};
|
|
|
|
/** 仅在导入时解析来源文字;报价读取结构化区间,不解析展示标签。 */
|
|
export function parseCommissionDocument(
|
|
document: unknown,
|
|
): CommissionImportCategory[] {
|
|
if (Buffer.byteLength(JSON.stringify(document) ?? '') > 2 * 1024 * 1024)
|
|
fail('JSON 不能超过 2 MiB');
|
|
const parsed = documentSchema.safeParse(document);
|
|
if (!parsed.success)
|
|
return fail(
|
|
'JSON 格式不正确:应包含一级类目、二级类目和每类完整的三个价格档位',
|
|
);
|
|
const ids = new Set<string>();
|
|
const result: CommissionImportCategory[] = [];
|
|
for (const group of parsed.data.data) {
|
|
if (ids.has(group.cate_id)) fail(`类目 ID 重复:${group.cate_id}`);
|
|
ids.add(group.cate_id);
|
|
for (const item of group.children) {
|
|
if (ids.has(item.cate_id)) fail(`类目 ID 重复:${item.cate_id}`);
|
|
ids.add(item.cate_id);
|
|
const bands = item.children.map((tier, index) => {
|
|
const value = /^([123]),(\d{1,2}(?:\.\d{1,2})?)$/.exec(
|
|
tier.value,
|
|
);
|
|
const prefix = [
|
|
'售价 ≤ 1500₽',
|
|
'1500₽ < 售价 ≤ 5000₽',
|
|
'售价 > 5000₽',
|
|
][index];
|
|
const label = /^(.*?)\s*\((\d{1,2}(?:\.\d{1,2})?)%\)$/.exec(
|
|
tier.label,
|
|
);
|
|
if (
|
|
!value ||
|
|
!label ||
|
|
value[1] !== String(index + 1) ||
|
|
label[1].trim() !== prefix ||
|
|
!D(value[2]).eq(label[2])
|
|
)
|
|
return fail(
|
|
`${item.label}:档位或标签百分比与 value 不一致`,
|
|
);
|
|
return {
|
|
min_price: ['0', '1500', '5000'][index],
|
|
max_price: ['1500', '5000', null][index],
|
|
min_inclusive: false,
|
|
max_inclusive: index < 2,
|
|
commission_rate: D(value[2]).div(100).toFixed(6),
|
|
};
|
|
});
|
|
result.push({
|
|
source_category_id: item.cate_id,
|
|
parent_source_id: group.cate_id,
|
|
name: item.label,
|
|
parent_name: group.label,
|
|
bands,
|
|
status: 'eligible',
|
|
issues: [],
|
|
change: 'added',
|
|
mapped_types: 0,
|
|
});
|
|
}
|
|
}
|
|
if (result.length > 2000) fail('二级类目不能超过 2,000 个');
|
|
return result;
|
|
}
|