export type RFMScore = { R: number; F: number; M: number; total: number; grade: "S" | "A" | "B" | "C" | "D" } export type AnalyzeInput = { user_id: string last_active: string interactions: number amount: number chat_logs?: string[] source?: string } export type AnalyzeResult = { user_id: string rfm_score: RFMScore tags: { emotion?: "积极" | "中性" | "消极" behavior?: string[] intent?: "弱意图" | "中等意图" | "强意图" lifecycle?: "新用户" | "活跃用户" | "沉睡用户" | "流失风险" value?: "高" | "中" | "低" } weights: { R: number; F: number; M: number } created_at: string updated_at: string } type GroupSummary = { gradeCount: Record valueCount: Record<"高" | "中" | "低", number> lifecycleCount: Record } // 内存存储(后续可替换为 Mongo/ES) const store = new Map() let weights = { R: 0.5, F: 0.3, M: 0.2 } export function getWeights() { return weights } export function setWeights(w: Partial) { weights = { ...weights, ...w } } // utils function clamp(n: number, min: number, max: number) { return Math.max(min, Math.min(max, n)) } function daysFromNow(iso: string) { const d = new Date(iso).getTime() const now = Date.now() return Math.max(0, Math.floor((now - d) / (1000 * 60 * 60 * 24))) } // 评分 function scoreR(lastActiveISO: string): number { const days = daysFromNow(lastActiveISO) if (days <= 1) return 5 if (days <= 3) return 4 if (days <= 7) return 3 if (days <= 30) return 2 return 1 } function scoreF(interactions: number): number { if (interactions >= 30) return 5 if (interactions >= 15) return 4 if (interactions >= 7) return 3 if (interactions >= 3) return 2 return 1 } function scoreM(amount: number): number { if (amount >= 5000) return 5 if (amount >= 2000) return 4 if (amount >= 800) return 3 if (amount >= 200) return 2 return 1 } function gradeFromTotal(t: number): RFMScore["grade"] { if (t >= 4.5) return "S" if (t >= 3.8) return "A" if (t >= 3.0) return "B" if (t >= 2.2) return "C" return "D" } function analyzeEmotion(chat?: string[]): "积极" | "中性" | "消极" | undefined { if (!chat || chat.length === 0) return undefined const joined = chat.join(" ") if (/[好棒|满意|喜欢|👍|推荐]/.test(joined)) return "积极" if (/[差|不行|失望|退款|投诉]/.test(joined)) return "消极" return "中性" } function analyzeIntent(chat?: string[], interactions?: number): "弱意图" | "中等意图" | "强意图" { const hasBuyWords = chat?.some((t) => /(购买|下单|价格|优惠|库存)/.test(t)) ?? false if (hasBuyWords && (interactions ?? 0) >= 20) return "强意图" if (hasBuyWords || (interactions ?? 0) >= 10) return "中等意图" return "弱意图" } function lifecycleByR(R: number): AnalyzeResult["tags"]["lifecycle"] { if (R >= 5) return "活跃用户" if (R >= 3) return "新用户" if (R === 2) return "沉睡用户" return "流失风险" } function valueByScore(total: number): "高" | "中" | "低" { if (total >= 4.0) return "高" if (total >= 2.8) return "中" return "低" } export function computeRFM(input: AnalyzeInput): RFMScore { const R = scoreR(input.last_active) const F = scoreF(input.interactions) const M = scoreM(input.amount) const total = clamp(R * weights.R + F * weights.F + M * weights.M, 1, 5) const grade = gradeFromTotal(total) return { R, F, M, total: Number(total.toFixed(2)), grade } } export function analyzeUser(input: AnalyzeInput): AnalyzeResult { const rfm = computeRFM(input) const tags: AnalyzeResult["tags"] = { emotion: analyzeEmotion(input.chat_logs), behavior: [], intent: analyzeIntent(input.chat_logs, input.interactions), lifecycle: lifecycleByR(rfm.R), value: valueByScore(rfm.total), } if (input.interactions >= 20) tags.behavior?.push("高频互动") if (input.amount >= 1000) tags.behavior?.push("高消费偏好") if (input.source === "wechat") tags.behavior?.push("微信渠道") if (input.source === "douyin") tags.behavior?.push("短视频渠道") const now = new Date().toISOString() const result: AnalyzeResult = { user_id: input.user_id, rfm_score: rfm, tags, weights, created_at: store.has(input.user_id) ? store.get(input.user_id)!.created_at : now, updated_at: now, } store.set(input.user_id, result) return result } export function getUserTags(user_id: string): AnalyzeResult | null { return store.get(user_id) ?? null } export function getGroupSummary(): GroupSummary { const grades: GroupSummary["gradeCount"] = { S: 0, A: 0, B: 0, C: 0, D: 0 } const values: GroupSummary["valueCount"] = { 高: 0, 中: 0, 低: 0 } const lifecycle: Record = {} store.forEach((r) => { grades[r.rfm_score.grade]++ if (r.tags.value) values[r.tags.value]++ const lc = r.tags.lifecycle ?? "未知" lifecycle[lc] = (lifecycle[lc] || 0) + 1 }) return { gradeCount: grades, valueCount: values, lifecycleCount: lifecycle } } export function dumpCsv(): string { const headers = ["user_id", "R", "F", "M", "total", "grade", "emotion", "intent", "lifecycle", "value"].join(",") const rows: string[] = [headers] store.forEach((r) => { rows.push( [ r.user_id, r.rfm_score.R, r.rfm_score.F, r.rfm_score.M, r.rfm_score.total, r.rfm_score.grade, r.tags.emotion ?? "", r.tags.intent ?? "", r.tags.lifecycle ?? "", r.tags.value ?? "", ].join(","), ) }) return rows.join("\n") }