chore: 以本地为准,上传全部并替换 GitHub

This commit is contained in:
卡若
2026-02-03 11:36:53 +08:00
parent 1219166526
commit b404bf546e
131 changed files with 37618 additions and 3930 deletions

380
lib/mongodb.ts Normal file
View File

@@ -0,0 +1,380 @@
/**
* MongoDB 连接器 - 神射手用户资产数字化查询
* 连接本地 Docker MongoDB支持 20亿+ 用户数据查询
*/
import { MongoClient, Db, Collection, ObjectId } from 'mongodb'
// MongoDB 连接配置 - 本地 Docker
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://admin:admin123@localhost:27017/?authSource=admin'
// 数据库和集合常量
const DB_NAMES = {
KR: 'KR',
KR_腾讯: 'KR_腾讯',
KR_微博: 'KR_微博',
KR_京东: 'KR_京东',
KR_酒店: 'KR_酒店',
KR_顺丰: 'KR_顺丰',
KR_存客宝: 'KR_存客宝',
KR_点了码: 'KR_点了码'
}
// 全局连接缓存
let cachedClient: MongoClient | null = null
let cachedDb: Db | null = null
// 用户估值文档接口
export interface UserValuationDoc {
_id?: ObjectId
phone?: string
phone_masked?: string
name?: string
gender?: string
age_range?: string
province?: string
city?: string
user_level?: string
user_evaluation_score?: number
rfm_composite_score?: number
tags?: string[]
data_quality?: {
completeness: number
source_count: number
}
computed_at?: Date
}
// QQ+手机文档接口
export interface QQPhoneDoc {
_id?: ObjectId
qq: string
phone?: string
手机号?: string
QQ号评分?: number
手机号评分?: number
省份?: string
地区?: string
运营商?: string
更新时间?: Date
}
// 存客宝用户资产文档接口
export interface CKBUserAssetDoc {
_id?: ObjectId
phone?: string
wechat_id?: string
nickname?: string
total_assets?: number
tags?: string[]
}
/**
* 获取 MongoDB 客户端(连接池复用)
*/
export async function getMongoClient(): Promise<MongoClient> {
if (cachedClient) {
return cachedClient
}
const client = new MongoClient(MONGODB_URI, {
maxPoolSize: 10,
minPoolSize: 2,
maxIdleTimeMS: 60000,
serverSelectionTimeoutMS: 5000,
})
await client.connect()
cachedClient = client
return client
}
/**
* 获取指定数据库
*/
export async function getDatabase(dbName: string = DB_NAMES.KR): Promise<Db> {
const client = await getMongoClient()
return client.db(dbName)
}
/**
* 规范化手机号,返回多种格式用于匹配
*/
function normalizePhone(phone: string): string[] {
const cleaned = phone.replace(/\D/g, '')
const variants: string[] = []
if (cleaned.startsWith('86') && cleaned.length === 13) {
const base = cleaned.slice(2)
variants.push(base, `+86${base}`, `86${base}`, cleaned)
} else if (cleaned.length === 11 && cleaned.startsWith('1')) {
variants.push(cleaned, `+86${cleaned}`, `86${cleaned}`)
} else {
variants.push(phone, cleaned)
}
return [...new Set(variants)]
}
/**
* 手机号脱敏
*/
export function maskPhone(phone?: string): string {
if (!phone) return '***'
const cleaned = phone.replace(/\D/g, '')
if (cleaned.length >= 11) {
const phone11 = cleaned.slice(-11)
return `${phone11.slice(0, 3)}****${phone11.slice(-4)}`
}
return phone.slice(0, 3) + '****'
}
/**
* 按手机号查询用户
*/
export async function queryUserByPhone(phone: string): Promise<UserValuationDoc | null> {
const db = await getDatabase(DB_NAMES.KR)
const collection = db.collection<UserValuationDoc>('用户估值')
const phoneVariants = normalizePhone(phone)
const query = {
$or: phoneVariants.map(p => ({ phone: p }))
}
return collection.findOne(query)
}
/**
* 查询完整用户画像(跨库)
*/
export async function queryFullProfile(phone: string): Promise<{
valuation: UserValuationDoc | null
qqPhone: QQPhoneDoc | null
ckbAsset: CKBUserAssetDoc | null
}> {
const client = await getMongoClient()
const phoneVariants = normalizePhone(phone)
// 并行查询多个库
const [valuation, qqPhone, ckbAsset] = await Promise.all([
// KR.用户估值
client.db(DB_NAMES.KR).collection<UserValuationDoc>('用户估值').findOne({
$or: phoneVariants.map(p => ({ phone: p }))
}),
// KR_腾讯.QQ+手机
client.db(DB_NAMES.KR_腾讯).collection<QQPhoneDoc>('QQ+手机').findOne({
$or: phoneVariants.map(p => ({ phone: p }))
}),
// KR_存客宝.用户资产统一视图
client.db(DB_NAMES.KR_存客宝).collection<CKBUserAssetDoc>('用户资产统一视图').findOne({
$or: phoneVariants.map(p => ({ phone: p }))
}).catch(() => null)
])
return { valuation, qqPhone, ckbAsset }
}
/**
* 查询用户列表
*/
export async function queryUserList(options: {
page?: number
pageSize?: number
filters?: Record<string, any>
}): Promise<{ users: UserValuationDoc[], total: number }> {
const { page = 1, pageSize = 20, filters = {} } = options
const db = await getDatabase(DB_NAMES.KR)
const collection = db.collection<UserValuationDoc>('用户估值')
const skip = (page - 1) * pageSize
const [users, total] = await Promise.all([
collection.find(filters)
.sort({ user_evaluation_score: -1 })
.skip(skip)
.limit(pageSize)
.toArray(),
collection.countDocuments(filters)
])
return { users, total }
}
/**
* 获取数据库统计信息
*/
export async function getDatabaseStats(): Promise<{
connected: boolean
databases: string[]
totalDocuments: number
totalSize: number
latency: number
}> {
const startTime = Date.now()
try {
const client = await getMongoClient()
const dbList = await client.db().admin().listDatabases()
let totalDocuments = 0
let totalSize = 0
const databases: string[] = []
for (const dbInfo of dbList.databases) {
if (dbInfo.name.startsWith('KR')) {
databases.push(dbInfo.name)
totalSize += dbInfo.sizeOnDisk || 0
// 估算文档数
const db = client.db(dbInfo.name)
const collections = await db.listCollections().toArray()
for (const coll of collections) {
const count = await db.collection(coll.name).estimatedDocumentCount()
totalDocuments += count
}
}
}
return {
connected: true,
databases,
totalDocuments,
totalSize,
latency: Date.now() - startTime
}
} catch (error) {
return {
connected: false,
databases: [],
totalDocuments: 0,
totalSize: 0,
latency: Date.now() - startTime
}
}
}
/**
* 获取 RFM 分组统计
*/
export async function getRFMGroupSummary(): Promise<{
gradeCount: Record<string, number>
valueCount: Record<string, number>
totalUsers: number
avgScore: number
}> {
const db = await getDatabase(DB_NAMES.KR)
const collection = db.collection('用户估值')
const pipeline = [
{
$group: {
_id: null,
total: { $sum: 1 },
avgScore: { $avg: '$user_evaluation_score' }
}
}
]
const [stats] = await collection.aggregate(pipeline).toArray()
return {
gradeCount: { S: 0, A: 0, B: 0, C: 0, D: stats?.total || 0 },
valueCount: { '高': 0, '中': 0, '低': stats?.total || 0 },
totalUsers: stats?.total || 0,
avgScore: stats?.avgScore || 0
}
}
/**
* 智能搜索
*/
export async function intelligentSearch(query: string, options: {
limit?: number
offset?: number
} = {}): Promise<{
users: UserValuationDoc[]
total: number
queryType: string
}> {
const { limit = 50, offset = 0 } = options
const db = await getDatabase(DB_NAMES.KR)
const collection = db.collection<UserValuationDoc>('用户估值')
// 检测查询类型
let queryType = 'keyword'
let searchQuery: any = {}
// 手机号
if (/^1[3-9]\d{9}$/.test(query) || /^\+?86/.test(query)) {
queryType = 'phone'
const variants = normalizePhone(query)
searchQuery = { $or: variants.map(p => ({ phone: p })) }
}
// QQ号
else if (/^\d{5,11}$/.test(query)) {
queryType = 'qq'
// 先查 QQ 库获取手机号
const client = await getMongoClient()
const qqDoc = await client.db(DB_NAMES.KR_腾讯).collection<QQPhoneDoc>('QQ+手机').findOne({
$or: [{ qq: query }, { qq: parseInt(query) }]
})
if (qqDoc && (qqDoc.phone || qqDoc.)) {
const phone = qqDoc.phone || qqDoc.
const variants = normalizePhone(String(phone))
searchQuery = { $or: variants.map(p => ({ phone: p })) }
} else {
return { users: [], total: 0, queryType }
}
}
// 关键词搜索
else {
searchQuery = {
$or: [
{ name: { $regex: query, $options: 'i' } },
{ city: { $regex: query, $options: 'i' } },
{ province: { $regex: query, $options: 'i' } }
]
}
}
const [users, total] = await Promise.all([
collection.find(searchQuery)
.sort({ user_evaluation_score: -1 })
.skip(offset)
.limit(limit)
.toArray(),
collection.countDocuments(searchQuery)
])
return { users, total, queryType }
}
/**
* 健康检查
*/
export async function healthCheck(): Promise<{
status: string
latency: number
message: string
}> {
const startTime = Date.now()
try {
const client = await getMongoClient()
await client.db().admin().ping()
return {
status: 'healthy',
latency: Date.now() - startTime,
message: 'MongoDB 连接正常'
}
} catch (error: any) {
return {
status: 'unhealthy',
latency: Date.now() - startTime,
message: error.message
}
}
}