86 lines
3.3 KiB
TypeScript
86 lines
3.3 KiB
TypeScript
|
|
import { Router } from 'express';
|
||
|
|
import { z } from 'zod';
|
||
|
|
import { ingestAuth } from '../middleware/ingestAuth.js';
|
||
|
|
import { ingestLimiter } from '../middleware/rateLimit.js';
|
||
|
|
import { ingest } from '../services/ingest.service.js';
|
||
|
|
|
||
|
|
const MessageSchema = z.object({
|
||
|
|
messageId: z.string().min(1),
|
||
|
|
sendTime: z.string().datetime(),
|
||
|
|
sender: z.string().optional(),
|
||
|
|
direction: z.enum(['incoming', 'outgoing', 'system']),
|
||
|
|
content: z.string(),
|
||
|
|
translation: z.string().optional().default(''),
|
||
|
|
});
|
||
|
|
|
||
|
|
const BehaviorSchema = z
|
||
|
|
.object({
|
||
|
|
productViews: z.number().nullable().optional(),
|
||
|
|
validInquiries: z.number().nullable().optional(),
|
||
|
|
validRfq: z.number().nullable().optional(),
|
||
|
|
loginDays: z.number().nullable().optional(),
|
||
|
|
spamInquiries: z.number().nullable().optional(),
|
||
|
|
blacklistCount: z.number().nullable().optional(),
|
||
|
|
topIndustry: z.string().nullable().optional(),
|
||
|
|
})
|
||
|
|
.optional();
|
||
|
|
|
||
|
|
const PayloadSchema = z.object({
|
||
|
|
inquiry: z.object({
|
||
|
|
inquiryId: z.string().min(1),
|
||
|
|
manager: z.string().optional(),
|
||
|
|
createTime: z.string().datetime(),
|
||
|
|
updateTime: z.string().datetime(),
|
||
|
|
status: z.string().optional(),
|
||
|
|
msgPreview: z.string().optional(),
|
||
|
|
isFlagged: z.boolean().optional().default(false),
|
||
|
|
}),
|
||
|
|
customer: z
|
||
|
|
.object({
|
||
|
|
customerId: z.string().min(1),
|
||
|
|
name: z.string().optional(),
|
||
|
|
country: z.string().nullable().optional(),
|
||
|
|
email: z.string().nullable().optional(),
|
||
|
|
landline: z.string().nullable().optional(),
|
||
|
|
businessType: z.string().nullable().optional(),
|
||
|
|
salesPlatform: z.string().nullable().optional(),
|
||
|
|
website: z.string().nullable().optional(),
|
||
|
|
phone: z.string().nullable().optional(),
|
||
|
|
company: z.string().nullable().optional(),
|
||
|
|
registerTime: z.string().nullable().optional(), // 页面是 "2016-08-27"(日期非 ISO),放宽
|
||
|
|
avatarUrl: z.string().optional(),
|
||
|
|
buyerTags: z.string().optional(),
|
||
|
|
buyerFeatures: z.string().optional(),
|
||
|
|
recentInquiryProducts: z.array(z.string()).optional(),
|
||
|
|
storeInquiryProduct: z.string().nullable().optional(),
|
||
|
|
storeInquiryQty: z.string().nullable().optional(),
|
||
|
|
storeVisitProducts: z.array(z.object({ img: z.string().optional(), url: z.string().optional(), productId: z.string().optional() })).optional(),
|
||
|
|
behavior: BehaviorSchema,
|
||
|
|
})
|
||
|
|
.nullable(),
|
||
|
|
messages: z.array(MessageSchema).min(0), // 阶段1:允许空(先走通列表+客户+行为,聊天后续补充)
|
||
|
|
});
|
||
|
|
|
||
|
|
export const ingestRouter = Router();
|
||
|
|
|
||
|
|
ingestRouter.post('/', ingestLimiter, ingestAuth, async (req, res, next) => {
|
||
|
|
const parsed = PayloadSchema.safeParse(req.body);
|
||
|
|
if (!parsed.success) {
|
||
|
|
// format() 显示完整 path(如 messages.0.sendTime),便于定位 null 字段
|
||
|
|
console.error('[ingest 400]', JSON.stringify(parsed.error.format()));
|
||
|
|
return res
|
||
|
|
.status(400)
|
||
|
|
.json({ error: 'invalid payload', issues: parsed.error.flatten() });
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const d = parsed.data;
|
||
|
|
console.log(`[ingest in] inquiry=${d.inquiry?.inquiryId} customer=${d.customer?.customerId ?? '-'} messages=${d.messages?.length ?? 0}`);
|
||
|
|
const result = await ingest(d);
|
||
|
|
console.log(`[ingest ok] inquiry=${d.inquiry?.inquiryId} -> ${JSON.stringify(result)}`);
|
||
|
|
res.json(result);
|
||
|
|
} catch (e) {
|
||
|
|
console.error('[ingest err] inquiry=', parsed.data.inquiry?.inquiryId, e);
|
||
|
|
next(e);
|
||
|
|
}
|
||
|
|
});
|