Files
users/services/rfm-mongodb-service.ts

250 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 }
}
}