312 lines
7.0 KiB
JavaScript
312 lines
7.0 KiB
JavaScript
const User = require('../models/User');
|
|
const Appointment = require('../models/Appointment');
|
|
const Invitation = require('../models/Invitation');
|
|
const Withdrawal = require('../models/Withdrawal');
|
|
const LoginHistory = require('../models/LoginHistory');
|
|
const { Op } = require('sequelize');
|
|
const { Parser } = require('json2csv');
|
|
|
|
/**
|
|
* Admin User Service
|
|
* Handles user management operations for admin panel
|
|
*/
|
|
|
|
/**
|
|
* Get paginated user list with search and filters
|
|
* @param {Object} options - Query options
|
|
* @returns {Promise<Object>} User list with pagination
|
|
*/
|
|
const getUserList = async (options = {}) => {
|
|
const {
|
|
page = 1,
|
|
limit = 20,
|
|
search = '',
|
|
status = '',
|
|
language = '',
|
|
sortBy = 'createdAt',
|
|
sortOrder = 'DESC',
|
|
} = options;
|
|
|
|
const offset = (page - 1) * limit;
|
|
|
|
// Build where clause
|
|
const where = {};
|
|
|
|
// Search by nickname, real name, phone, or wechat ID
|
|
if (search) {
|
|
where[Op.or] = [
|
|
{ nickname: { [Op.like]: `%${search}%` } },
|
|
{ realName: { [Op.like]: `%${search}%` } },
|
|
{ phone: { [Op.like]: `%${search}%` } },
|
|
{ wechatId: { [Op.like]: `%${search}%` } },
|
|
];
|
|
}
|
|
|
|
// Filter by status
|
|
if (status) {
|
|
where.status = status;
|
|
}
|
|
|
|
// Filter by language
|
|
if (language) {
|
|
where.language = language;
|
|
}
|
|
|
|
// Query users
|
|
const { count, rows: users } = await User.findAndCountAll({
|
|
where,
|
|
limit: parseInt(limit),
|
|
offset,
|
|
order: [[sortBy, sortOrder]],
|
|
attributes: [
|
|
'id',
|
|
'uid',
|
|
'nickname',
|
|
'avatar',
|
|
'realName',
|
|
'phone',
|
|
'whatsapp',
|
|
'wechatId',
|
|
'language',
|
|
'balance',
|
|
'invitationCode',
|
|
'status',
|
|
'createdAt',
|
|
'updatedAt',
|
|
],
|
|
});
|
|
|
|
return {
|
|
users,
|
|
pagination: {
|
|
total: count,
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
totalPages: Math.ceil(count / limit),
|
|
},
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Get detailed user information
|
|
* @param {String} userId - User ID
|
|
* @returns {Promise<Object>} User details with related data
|
|
*/
|
|
const getUserDetails = async (userId) => {
|
|
// Get user
|
|
const user = await User.findByPk(userId, {
|
|
attributes: [
|
|
'id',
|
|
'uid',
|
|
'wechatOpenId',
|
|
'nickname',
|
|
'avatar',
|
|
'realName',
|
|
'phone',
|
|
'whatsapp',
|
|
'wechatId',
|
|
'language',
|
|
'balance',
|
|
'invitationCode',
|
|
'invitedBy',
|
|
'status',
|
|
'createdAt',
|
|
'updatedAt',
|
|
],
|
|
});
|
|
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
// Get inviter information if exists
|
|
let inviter = null;
|
|
if (user.invitedBy) {
|
|
inviter = await User.findByPk(user.invitedBy, {
|
|
attributes: ['id', 'nickname', 'realName'],
|
|
});
|
|
}
|
|
|
|
// Get appointment statistics
|
|
const appointmentStats = await Appointment.findAll({
|
|
where: { userId },
|
|
attributes: [
|
|
'status',
|
|
[require('sequelize').fn('COUNT', require('sequelize').col('id')), 'count'],
|
|
],
|
|
group: ['status'],
|
|
raw: true,
|
|
});
|
|
|
|
const appointmentCounts = {
|
|
total: 0,
|
|
pending: 0,
|
|
confirmed: 0,
|
|
'in-progress': 0,
|
|
completed: 0,
|
|
cancelled: 0,
|
|
};
|
|
|
|
appointmentStats.forEach((stat) => {
|
|
appointmentCounts[stat.status] = parseInt(stat.count);
|
|
appointmentCounts.total += parseInt(stat.count);
|
|
});
|
|
|
|
// Get invitation statistics
|
|
const invitationStats = await Invitation.findAll({
|
|
where: { inviterId: userId },
|
|
attributes: [
|
|
[require('sequelize').fn('COUNT', require('sequelize').col('id')), 'totalInvites'],
|
|
[require('sequelize').fn('SUM', require('sequelize').col('reward_amount')), 'totalRewards'],
|
|
],
|
|
raw: true,
|
|
});
|
|
|
|
const invitationCounts = {
|
|
totalInvites: parseInt(invitationStats[0]?.totalInvites || 0),
|
|
totalRewards: parseFloat(invitationStats[0]?.totalRewards || 0),
|
|
};
|
|
|
|
// Get withdrawal statistics
|
|
const withdrawalStats = await Withdrawal.findAll({
|
|
where: { userId },
|
|
attributes: [
|
|
'status',
|
|
[require('sequelize').fn('COUNT', require('sequelize').col('id')), 'count'],
|
|
[require('sequelize').fn('SUM', require('sequelize').col('amount')), 'total'],
|
|
],
|
|
group: ['status'],
|
|
raw: true,
|
|
});
|
|
|
|
const withdrawalCounts = {
|
|
total: 0,
|
|
totalAmount: 0,
|
|
waiting: 0,
|
|
processing: 0,
|
|
completed: 0,
|
|
rejected: 0,
|
|
};
|
|
|
|
withdrawalStats.forEach((stat) => {
|
|
withdrawalCounts[stat.status] = parseInt(stat.count);
|
|
withdrawalCounts.total += parseInt(stat.count);
|
|
withdrawalCounts.totalAmount += parseFloat(stat.total || 0);
|
|
});
|
|
|
|
// Get recent login history
|
|
const loginHistory = await LoginHistory.findAll({
|
|
where: { userId, userType: 'user' },
|
|
limit: 10,
|
|
order: [['loginAt', 'DESC']],
|
|
attributes: ['id', 'ipAddress', 'userAgent', 'deviceInfo', 'loginAt'],
|
|
});
|
|
|
|
return {
|
|
user: user.toJSON(),
|
|
inviter,
|
|
appointments: appointmentCounts,
|
|
invitations: invitationCounts,
|
|
withdrawals: withdrawalCounts,
|
|
loginHistory,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Update user status
|
|
* @param {String} userId - User ID
|
|
* @param {String} status - New status (active/suspended)
|
|
* @returns {Promise<Object>} Updated user
|
|
*/
|
|
const updateUserStatus = async (userId, status) => {
|
|
const user = await User.findByPk(userId);
|
|
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
if (!['active', 'suspended'].includes(status)) {
|
|
throw new Error('Invalid status value');
|
|
}
|
|
|
|
user.status = status;
|
|
await user.save();
|
|
|
|
return user;
|
|
};
|
|
|
|
/**
|
|
* Export user data to CSV
|
|
* @param {Object} options - Query options (same as getUserList)
|
|
* @returns {Promise<String>} CSV string
|
|
*/
|
|
const exportUsersToCSV = async (options = {}) => {
|
|
// Get all users matching criteria (no pagination)
|
|
const { search = '', status = '', language = '' } = options;
|
|
|
|
const where = {};
|
|
|
|
if (search) {
|
|
where[Op.or] = [
|
|
{ nickname: { [Op.like]: `%${search}%` } },
|
|
{ realName: { [Op.like]: `%${search}%` } },
|
|
{ phone: { [Op.like]: `%${search}%` } },
|
|
{ wechatId: { [Op.like]: `%${search}%` } },
|
|
];
|
|
}
|
|
|
|
if (status) {
|
|
where.status = status;
|
|
}
|
|
|
|
if (language) {
|
|
where.language = language;
|
|
}
|
|
|
|
const users = await User.findAll({
|
|
where,
|
|
order: [['createdAt', 'DESC']],
|
|
attributes: [
|
|
'id',
|
|
'nickname',
|
|
'realName',
|
|
'phone',
|
|
'whatsapp',
|
|
'wechatId',
|
|
'language',
|
|
'balance',
|
|
'invitationCode',
|
|
'status',
|
|
'createdAt',
|
|
],
|
|
});
|
|
|
|
// Convert to plain objects
|
|
const userData = users.map((user) => ({
|
|
ID: user.id,
|
|
Nickname: user.nickname || '',
|
|
'Real Name': user.realName || '',
|
|
Phone: user.phone || '',
|
|
WhatsApp: user.whatsapp || '',
|
|
'WeChat ID': user.wechatId || '',
|
|
Language: user.language,
|
|
Balance: user.balance,
|
|
'Invitation Code': user.invitationCode || '',
|
|
Status: user.status,
|
|
'Created At': user.createdAt.toISOString(),
|
|
}));
|
|
|
|
// Convert to CSV
|
|
const json2csvParser = new Parser();
|
|
const csv = json2csvParser.parse(userData);
|
|
|
|
return csv;
|
|
};
|
|
|
|
module.exports = {
|
|
getUserList,
|
|
getUserDetails,
|
|
updateUserStatus,
|
|
exportUsersToCSV,
|
|
};
|