import { randomUUID } from "crypto" export type UserStatus = "活跃" | "沉睡" | "流失风险" export interface User { id: string name: string email: string phone: string avatar: string tags: string[] status: UserStatus rfmScore: number createdAt: string lastActiveAt: string // 画像扩展 city: string store: string privateDomain: string project: string team: string persona: string[] source: "douyin" | "cunkebao_form_submit" | "touchkebao_call_in" | "pos_api" | "manual" } const cities = ["北京", "上海", "广州", "深圳", "成都", "杭州", "苏州", "武汉", "西安", "长沙"] const stores = ["门店A1", "门店A2", "门店B1", "门店C3", "门店D2"] const privateDomains = ["企业微信私域1", "社群A", "社群B", "公众号粉丝池"] const projects = ["项目X", "项目Y", "项目Z", "活动618"] const teams = ["团队一组", "团队二组", "团队增长", "客户成功"] const personas = ["技术爱好者", "夜猫子", "内容创作者", "效率提升", "价格敏感", "品牌忠诚"] const tagPool = [ "高价值", "近7日活跃", "新客", "回流", "社群达人", "潜在复购", "高互动", "低客单", "私域粉", "公众号粉", ] const statusPool: UserStatus[] = ["活跃", "沉睡", "流失风险"] const avatars = [ "/user-avatar-zhangsan.png", "/user-avatar-lisi.png", "/avatar-wanglei.png", "/generic-user-avatar.png", "/wechat-avatar-1.png", "/wechat-avatar-2.png", "/wechat-avatar-3.png", ] const rand = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min const pick = (arr: T[]) => arr[rand(0, arr.length - 1)] function toPinyinLike(name: string) { const map: Record = { 张: "zhang", 李: "li", 王: "wang", 赵: "zhao", 刘: "liu", 陈: "chen", 杨: "yang", 黄: "huang", 周: "zhou", 吴: "wu", 徐: "xu", 孙: "sun", 胡: "hu", 朱: "zhu", 高: "gao", 林: "lin", 何: "he", 郭: "guo", 马: "ma", 罗: "luo", } return name .split("") .map((c) => map[c] ?? "u") .join("") } function randomPhone() { const prefixes = [ "139", "138", "137", "136", "135", "188", "187", "186", "185", "183", "182", "159", "158", "157", "156", "155", ] return `${pick(prefixes)}${rand(1000, 9999)}${rand(1000, 9999)}` } function randomTags() { const count = rand(2, 4) const s = new Set() while (s.size < count) s.add(pick(tagPool)) return Array.from(s) } function timeNearNow(daysSpan = 90) { const now = Date.now() const offset = rand(0, daysSpan * 86400000) return new Date(now - offset).toISOString() } let cache: User[] | null = null function seed(n = 240) { const familyNames = [ "张", "李", "王", "赵", "刘", "陈", "杨", "黄", "周", "吴", "徐", "孙", "胡", "朱", "高", "林", "何", "郭", "马", "罗", ] const givenNames = [ "伟", "芳", "娜", "敏", "静", "秀英", "丽", "强", "磊", "军", "洋", "艳", "勇", "杰", "娟", "涛", "明", "超", "霞", "平", "俊", "凯", "佳", "鑫", "鹏", "晨", "倩", "颖", "梅", "慧", "雪", "宇", "涵", "宁", "璐", "龙", "震", "航", "璟", "钰", ] const list: User[] = [] for (let i = 0; i < n; i++) { const name = `${pick(familyNames)}${pick(givenNames)}${Math.random() < 0.2 ? pick(givenNames) : ""}` const email = `${toPinyinLike(name)}${rand(1, 99)}@example.com` const u: User = { id: randomUUID(), name, email, phone: randomPhone(), avatar: avatars[i % avatars.length], tags: randomTags(), status: pick(statusPool), rfmScore: rand(20, 95), createdAt: timeNearNow(180), lastActiveAt: timeNearNow(15), city: pick(cities), store: pick(stores), privateDomain: pick(privateDomains), project: pick(projects), team: pick(teams), persona: [pick(personas), Math.random() > 0.6 ? pick(personas) : undefined].filter(Boolean) as string[], source: pick(["douyin", "cunkebao_form_submit", "touchkebao_call_in", "pos_api", "manual"]), } list.push(u) } return list } export function getUsersStore() { if (!cache) cache = seed() return cache } export interface QueryParams { q?: string tags?: string[] status?: UserStatus[] rfmMin?: number rfmMax?: number city?: string[] persona?: string[] source?: string[] page?: number pageSize?: number } export function queryUsers(params: QueryParams) { const { q, tags, status, rfmMin = 0, rfmMax = 100, city, persona, source, page = 1, pageSize = 20 } = params let list = getUsersStore() if (q && q.trim()) { const s = q.trim().toLowerCase() list = list.filter( (u) => u.name.toLowerCase().includes(s) || u.email.toLowerCase().includes(s) || u.phone.includes(s) || u.tags.some((t) => t.toLowerCase().includes(s)) || u.persona.some((p) => p.toLowerCase().includes(s)), ) } if (tags?.length) list = list.filter((u) => tags.every((t) => u.tags.includes(t))) if (status?.length) list = list.filter((u) => status.includes(u.status)) list = list.filter((u) => u.rfmScore >= rfmMin && u.rfmScore <= rfmMax) if (city?.length) list = list.filter((u) => city.includes(u.city)) if (persona?.length) list = list.filter((u) => u.persona.some((p) => persona.includes(p))) if (source?.length) list = list.filter((u) => source.includes(u.source)) const total = list.length const start = (page - 1) * pageSize const data = list.slice(start, start + pageSize) return { data, pagination: { page, pageSize, total, totalPages: Math.max(1, Math.ceil(total / pageSize)) }, } } export function addUser(input: Partial) { const list = getUsersStore() const now = new Date().toISOString() const u: User = { id: randomUUID(), name: input.name ?? "新用户", email: input.email ?? "user@example.com", phone: input.phone ?? "13900000000", avatar: input.avatar ?? avatars[0], tags: input.tags ?? ["新客"], status: input.status ?? "活跃", rfmScore: input.rfmScore ?? 60, createdAt: now, lastActiveAt: now, city: input.city ?? pick(cities), store: input.store ?? pick(stores), privateDomain: input.privateDomain ?? pick(privateDomains), project: input.project ?? pick(projects), team: input.team ?? pick(teams), persona: input.persona ?? [pick(personas)], source: input.source ?? "manual", } list.unshift(u) return u }