Files
users/lib/mock-users.ts

294 lines
6.7 KiB
TypeScript
Raw Normal View History

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 = <T,>(arr: T[]) => arr[rand(0, arr.length - 1)]
function toPinyinLike(name: string) {
const map: Record<string, string> = {
: "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<string>()
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<User>) {
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
}