250 lines
6.6 KiB
TypeScript
250 lines
6.6 KiB
TypeScript
/**
|
||
* RFM MongoDB 服务
|
||
* 从 MongoDB 获取真实 RFM 数据
|
||
*/
|
||
|
||
import {
|
||
getCollection,
|
||
getRFMGroupSummary,
|
||
getDistinctTags,
|
||
DB_NAMES,
|
||
COLLECTION_NAMES,
|
||
UserValuationDoc
|
||
} from '@/lib/mongodb'
|
||
|
||
export interface RFMStats {
|
||
gradeCount: Record<string, number>
|
||
valueCount: Record<string, number>
|
||
totalUsers: number
|
||
avgScore: number
|
||
}
|
||
|
||
/**
|
||
* 获取 RFM 分组统计(从 MongoDB)
|
||
*/
|
||
export async function getMongoRFMGroupSummary(): Promise<RFMStats> {
|
||
try {
|
||
const result = await getRFMGroupSummary()
|
||
|
||
// 转换为前端期望的格式
|
||
const gradeCount: Record<string, number> = {
|
||
'S': 0,
|
||
'A': 0,
|
||
'B': 0,
|
||
'C': 0,
|
||
'D': 0
|
||
}
|
||
|
||
const valueCount: Record<string, number> = {
|
||
'高': 0,
|
||
'中': 0,
|
||
'低': 0
|
||
}
|
||
|
||
// 映射用户等级到 S/A/B/C/D
|
||
for (const item of result.levels) {
|
||
const level = item.level
|
||
if (level === 'S' || level === '钻石用户' || level === 'S级用户') {
|
||
gradeCount['S'] += item.count
|
||
valueCount['高'] += item.count
|
||
} else if (level === 'A' || level === '黄金用户' || level === 'A级用户') {
|
||
gradeCount['A'] += item.count
|
||
valueCount['高'] += item.count
|
||
} else if (level === 'B' || level === '白银用户' || level === 'B级用户') {
|
||
gradeCount['B'] += item.count
|
||
valueCount['中'] += item.count
|
||
} else if (level === 'C' || level === '青铜用户' || level === 'C级用户') {
|
||
gradeCount['C'] += item.count
|
||
valueCount['中'] += item.count
|
||
} else {
|
||
gradeCount['D'] += item.count
|
||
valueCount['低'] += item.count
|
||
}
|
||
}
|
||
|
||
// 计算平均分
|
||
const coll = await getCollection<UserValuationDoc>(DB_NAMES.KR, COLLECTION_NAMES.USER_VALUATION)
|
||
const avgResult = await coll.aggregate([
|
||
{ $match: { rfm_composite_score: { $exists: true, $ne: null } } },
|
||
{ $group: { _id: null, avgScore: { $avg: '$rfm_composite_score' } } }
|
||
]).toArray()
|
||
|
||
const avgScore = avgResult[0]?.avgScore || 0
|
||
|
||
return {
|
||
gradeCount,
|
||
valueCount,
|
||
totalUsers: result.total,
|
||
avgScore: Math.round(avgScore * 100) / 100
|
||
}
|
||
} catch (error) {
|
||
console.error('RFM stats error:', error)
|
||
return {
|
||
gradeCount: { S: 0, A: 0, B: 0, C: 0, D: 0 },
|
||
valueCount: { '高': 0, '中': 0, '低': 0 },
|
||
totalUsers: 0,
|
||
avgScore: 0
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取用户 RFM 分析
|
||
*/
|
||
export async function analyzeUserRFM(phone: string): Promise<{
|
||
found: boolean
|
||
data: any
|
||
}> {
|
||
try {
|
||
const coll = await getCollection<UserValuationDoc>(DB_NAMES.KR, COLLECTION_NAMES.USER_VALUATION)
|
||
const user = await coll.findOne({ phone })
|
||
|
||
if (!user) {
|
||
return { found: false, data: null }
|
||
}
|
||
|
||
// 计算 RFM 等级
|
||
const rfmScore = user.rfm_composite_score || 0
|
||
let grade: 'S' | 'A' | 'B' | 'C' | 'D' = 'D'
|
||
if (rfmScore >= 4.5) grade = 'S'
|
||
else if (rfmScore >= 3.5) grade = 'A'
|
||
else if (rfmScore >= 2.5) grade = 'B'
|
||
else if (rfmScore >= 1.5) grade = 'C'
|
||
|
||
// 计算价值等级
|
||
let value: '高' | '中' | '低' = '低'
|
||
if (rfmScore >= 4.0) value = '高'
|
||
else if (rfmScore >= 2.8) value = '中'
|
||
|
||
// 生命周期标签
|
||
let lifecycle = '未知'
|
||
const rScore = user.rfm_r_score || 0
|
||
if (rScore >= 5) lifecycle = '活跃用户'
|
||
else if (rScore >= 3) lifecycle = '新用户'
|
||
else if (rScore === 2) lifecycle = '沉睡用户'
|
||
else lifecycle = '流失风险'
|
||
|
||
return {
|
||
found: true,
|
||
data: {
|
||
user_id: phone,
|
||
rfm_score: {
|
||
R: user.rfm_r_score || 0,
|
||
F: user.rfm_f_score || 0,
|
||
M: user.rfm_m_score || 0,
|
||
total: rfmScore,
|
||
grade
|
||
},
|
||
tags: {
|
||
lifecycle,
|
||
value,
|
||
behavior: user.tags || []
|
||
},
|
||
weights: { R: 0.3, F: 0.3, M: 0.4 },
|
||
raw: {
|
||
name: user.name,
|
||
province: user.province,
|
||
city: user.city,
|
||
userLevel: user.user_level,
|
||
totalAmount: user.total_amount,
|
||
orderCount: user.order_count
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Analyze user RFM error:', error)
|
||
return { found: false, data: null }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取 RFM 标签分布
|
||
*/
|
||
export async function getRFMTagsDistribution(): Promise<{
|
||
tags: { name: string, count: number }[]
|
||
}> {
|
||
try {
|
||
const tags = await getDistinctTags()
|
||
|
||
// 对每个标签计数
|
||
const coll = await getCollection<UserValuationDoc>(DB_NAMES.KR, COLLECTION_NAMES.USER_VALUATION)
|
||
|
||
const tagCounts: { name: string, count: number }[] = []
|
||
|
||
// 只统计前 20 个标签
|
||
for (const tag of tags.slice(0, 20)) {
|
||
const count = await coll.countDocuments({ tags: tag })
|
||
tagCounts.push({ name: tag, count })
|
||
}
|
||
|
||
// 按数量排序
|
||
tagCounts.sort((a, b) => b.count - a.count)
|
||
|
||
return { tags: tagCounts }
|
||
} catch (error) {
|
||
console.error('Tags distribution error:', error)
|
||
return { tags: [] }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取 RFM 用户列表
|
||
*/
|
||
export async function getRFMUserList(options: {
|
||
grade?: string
|
||
page?: number
|
||
pageSize?: number
|
||
}): Promise<{
|
||
users: any[]
|
||
total: number
|
||
}> {
|
||
const { grade, page = 1, pageSize = 20 } = options
|
||
|
||
try {
|
||
const coll = await getCollection<UserValuationDoc>(DB_NAMES.KR, COLLECTION_NAMES.USER_VALUATION)
|
||
|
||
const query: any = {}
|
||
|
||
if (grade) {
|
||
// 映射等级
|
||
if (grade === 'S') {
|
||
query.rfm_composite_score = { $gte: 4.5 }
|
||
} else if (grade === 'A') {
|
||
query.rfm_composite_score = { $gte: 3.5, $lt: 4.5 }
|
||
} else if (grade === 'B') {
|
||
query.rfm_composite_score = { $gte: 2.5, $lt: 3.5 }
|
||
} else if (grade === 'C') {
|
||
query.rfm_composite_score = { $gte: 1.5, $lt: 2.5 }
|
||
} else {
|
||
query.rfm_composite_score = { $lt: 1.5 }
|
||
}
|
||
}
|
||
|
||
const [users, total] = await Promise.all([
|
||
coll.find(query)
|
||
.skip((page - 1) * pageSize)
|
||
.limit(pageSize)
|
||
.sort({ rfm_composite_score: -1 })
|
||
.toArray(),
|
||
coll.countDocuments(query)
|
||
])
|
||
|
||
return {
|
||
users: users.map(u => ({
|
||
id: u._id?.toString(),
|
||
phone: u.phone,
|
||
phone_masked: u.phone_masked || (u.phone ? `${u.phone.slice(0, 3)}****${u.phone.slice(-4)}` : ''),
|
||
name: u.name,
|
||
rfmScore: u.rfm_composite_score,
|
||
userLevel: u.user_level,
|
||
province: u.province,
|
||
tags: u.tags
|
||
})),
|
||
total
|
||
}
|
||
} catch (error) {
|
||
console.error('RFM user list error:', error)
|
||
return { users: [], total: 0 }
|
||
}
|
||
}
|