refactor: restructure project into 5 core modules
Organize project by 5 core modules based on requirement docs. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
@@ -1,249 +0,0 @@
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user