diff --git a/admin/src/views/users/index.vue b/admin/src/views/users/index.vue index 342cc40..d898009 100644 --- a/admin/src/views/users/index.vue +++ b/admin/src/views/users/index.vue @@ -195,8 +195,16 @@ {{ userDetails.invitations.totalInvites }}
- 累计奖励 - ¥{{ parseFloat(userDetails.invitations.totalRewards || 0).toFixed(2) }} + 已付费人数 + {{ userDetails.invitations.paidInvites || 0 }} +
+
+ 累计奖励(¥) + ¥{{ parseFloat(userDetails.invitations.totalRewardsRmb || userDetails.invitations.totalRewards || 0).toFixed(2) }} +
+
+ 累计奖励(₱) + ₱{{ parseFloat(userDetails.invitations.totalRewardsPeso || 0).toFixed(2) }}
@@ -247,12 +255,100 @@ + + +
+

邀请用户记录

+ + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + {{ selectedInvitedUser.user?.nickname || '-' }} + {{ selectedInvitedUser.user?.uid || '-' }} + {{ selectedInvitedUser.orderCount }} + + ¥{{ selectedInvitedUser.totalCommissionRmb }} + ₱{{ selectedInvitedUser.totalCommissionPeso }} + + + +

订单记录

+ + + + + + + + + + + + +
+ +
@@ -286,6 +382,10 @@ const detailsDialogVisible = ref(false) const detailsLoading = ref(false) const userDetails = ref(null) +// Order details dialog state +const orderDetailsDialogVisible = ref(false) +const selectedInvitedUser = ref(null) + // Fetch user list async function fetchUsers() { loading.value = true @@ -380,6 +480,12 @@ function handleViewDetails(row) { fetchUserDetails(row.id) } +// Show order details for invited user +function showOrderDetails(invitedUser) { + selectedInvitedUser.value = invitedUser + orderDetailsDialogVisible.value = true +} + // Handle toggle status async function handleToggleStatus(row) { const newStatus = row.status === 'active' ? 'suspended' : 'active' @@ -568,10 +674,37 @@ onMounted(() => { } } + .invited-users-section { + margin-top: 20px; + + h4 { + margin-bottom: 12px; + color: #303133; + font-size: 14px; + } + + .reward-amount { + font-weight: 600; + color: #409eff; + margin-right: 8px; + + &.peso { + color: #67c23a; + } + } + } + .balance { font-weight: 600; color: #409eff; } } + + .order-details { + .reward-text { + font-weight: 600; + color: #67c23a; + } + } } diff --git a/backend/src/controllers/commissionController.js b/backend/src/controllers/commissionController.js index 11f4023..5a5ff1c 100644 --- a/backend/src/controllers/commissionController.js +++ b/backend/src/controllers/commissionController.js @@ -71,7 +71,38 @@ const getMyCommissionStats = async (req, res) => { } }; +/** + * Get invited users with their commission details + * GET /api/v1/commissions/invited-users + */ +const getInvitedUsers = async (req, res) => { + try { + const userId = req.user.id; + const { page, limit } = req.query; + + const result = await commissionService.getInvitedUsersWithCommissions(userId, { + page, + limit, + }); + + return res.status(200).json({ + success: true, + data: result, + }); + } catch (error) { + return res.status(500).json({ + success: false, + error: { + code: 'GET_INVITED_USERS_ERROR', + message: 'Failed to get invited users', + details: error.message, + }, + }); + } +}; + module.exports = { getMyCommissions, getMyCommissionStats, + getInvitedUsers, }; diff --git a/backend/src/routes/commissionRoutes.js b/backend/src/routes/commissionRoutes.js index 54be258..00d29e1 100644 --- a/backend/src/routes/commissionRoutes.js +++ b/backend/src/routes/commissionRoutes.js @@ -22,4 +22,11 @@ router.get('/', authenticateUser, commissionController.getMyCommissions); */ router.get('/stats', authenticateUser, commissionController.getMyCommissionStats); +/** + * @route GET /api/v1/commissions/invited-users + * @desc Get invited users with their commission details + * @access Private (User) + */ +router.get('/invited-users', authenticateUser, commissionController.getInvitedUsers); + module.exports = router; diff --git a/backend/src/services/adminUserService.js b/backend/src/services/adminUserService.js index bf561ee..3a6a538 100644 --- a/backend/src/services/adminUserService.js +++ b/backend/src/services/adminUserService.js @@ -3,6 +3,8 @@ const Appointment = require('../models/Appointment'); const Invitation = require('../models/Invitation'); const Withdrawal = require('../models/Withdrawal'); const LoginHistory = require('../models/LoginHistory'); +const Commission = require('../models/Commission'); +const PaymentOrder = require('../models/PaymentOrder'); const { Op } = require('sequelize'); const { Parser } = require('json2csv'); @@ -152,21 +154,118 @@ const getUserDetails = async (userId) => { 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'], - ], + // Get invitation statistics from Commission table (more accurate) + const commissions = await Commission.findAll({ + where: { inviterId: userId, status: 'credited' }, + attributes: ['commissionAmount', 'currency'], raw: true, }); + let totalRewardsRmb = 0; + let totalRewardsPeso = 0; + commissions.forEach(c => { + const amount = parseFloat(c.commissionAmount || 0); + if (c.currency === 'PHP') { + totalRewardsPeso += amount; + } else { + totalRewardsRmb += amount; + } + }); + + // Get invited users count + const invitedUsersCount = await User.count({ + where: { invitedBy: userId }, + }); + + // Get paid invites count + const paidInvitesCount = await Commission.count({ + where: { inviterId: userId, status: 'credited' }, + distinct: true, + col: 'invitee_id', + }); + const invitationCounts = { - totalInvites: parseInt(invitationStats[0]?.totalInvites || 0), - totalRewards: parseFloat(invitationStats[0]?.totalRewards || 0), + totalInvites: invitedUsersCount, + paidInvites: paidInvitesCount, + totalRewards: totalRewardsRmb, + totalRewardsRmb: totalRewardsRmb, + totalRewardsPeso: totalRewardsPeso, }; + // Get invited users with their commission details + const invitedUsers = await User.findAll({ + where: { invitedBy: userId }, + attributes: ['id', 'uid', 'nickname', 'avatar', 'createdAt'], + order: [['createdAt', 'DESC']], + limit: 50, + }); + + // Get commission details for each invited user + const invitedUsersWithCommissions = await Promise.all( + invitedUsers.map(async (invitee) => { + const userCommissions = await Commission.findAll({ + where: { + inviterId: userId, + inviteeId: invitee.id, + status: 'credited' + }, + include: [ + { + model: PaymentOrder, + as: 'paymentOrder', + attributes: ['id', 'orderNo', 'serviceContent', 'paymentTime', 'amountRmb', 'amountPeso'], + }, + ], + order: [['createdAt', 'DESC']], + }); + + let userTotalRmb = 0; + let userTotalPeso = 0; + let userPaymentRmb = 0; + let userPaymentPeso = 0; + + const orders = userCommissions.map(c => { + const commissionAmount = parseFloat(c.commissionAmount); + const paymentAmount = parseFloat(c.paymentAmount); + + if (c.currency === 'PHP') { + userTotalPeso += commissionAmount; + userPaymentPeso += paymentAmount; + } else { + userTotalRmb += commissionAmount; + userPaymentRmb += paymentAmount; + } + + return { + id: c.id, + orderNo: c.paymentOrder?.orderNo || '-', + serviceContent: c.paymentOrder?.serviceContent || '-', + paymentTime: c.paymentOrder?.paymentTime || c.createdAt, + paymentAmount: paymentAmount.toFixed(2), + commissionAmount: commissionAmount.toFixed(2), + currency: c.currency || 'RMB', + createdAt: c.createdAt, + }; + }); + + return { + user: { + id: invitee.id, + uid: invitee.uid, + nickname: invitee.nickname, + avatar: invitee.avatar, + registeredAt: invitee.createdAt, + }, + orderCount: orders.length, + orders, + totalPaymentRmb: userPaymentRmb.toFixed(2), + totalPaymentPeso: userPaymentPeso.toFixed(2), + totalCommissionRmb: userTotalRmb.toFixed(2), + totalCommissionPeso: userTotalPeso.toFixed(2), + }; + }) + ); + // Get withdrawal statistics const withdrawalStats = await Withdrawal.findAll({ where: { userId }, @@ -207,6 +306,7 @@ const getUserDetails = async (userId) => { inviter, appointments: appointmentCounts, invitations: invitationCounts, + invitedUsers: invitedUsersWithCommissions, withdrawals: withdrawalCounts, loginHistory, }; diff --git a/backend/src/services/commissionService.js b/backend/src/services/commissionService.js index 423e273..61afe38 100644 --- a/backend/src/services/commissionService.js +++ b/backend/src/services/commissionService.js @@ -125,19 +125,20 @@ const getCommissionsByInviter = async (inviterId, options = {}) => { const records = rows.map(commission => ({ id: commission.id, - invitee: { + invitee: commission.invitee ? { id: commission.invitee.id, uid: commission.invitee.uid, nickname: commission.invitee.nickname, avatar: commission.invitee.avatar, - }, - paymentOrder: { + } : null, + paymentOrder: commission.paymentOrder ? { id: commission.paymentOrder.id, orderNo: commission.paymentOrder.orderNo, serviceContent: commission.paymentOrder.serviceContent, paymentTime: commission.paymentOrder.paymentTime, - }, + } : null, paymentAmount: parseFloat(commission.paymentAmount).toFixed(2), + currency: commission.currency || 'RMB', commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`, commissionAmount: parseFloat(commission.commissionAmount).toFixed(2), createdAt: commission.createdAt, @@ -154,6 +155,104 @@ const getCommissionsByInviter = async (inviterId, options = {}) => { }; }; +/** + * Get invited users with their commission details (grouped by user) + * @param {string} inviterId - Inviter user ID + * @param {Object} options - Query options (page, limit) + * @returns {Object} Invited users with commission details + */ +const getInvitedUsersWithCommissions = async (inviterId, options = {}) => { + const { page = 1, limit = 20 } = options; + const offset = (page - 1) * limit; + + // Get all invited users + const { count, rows: invitedUsers } = await User.findAndCountAll({ + where: { invitedBy: inviterId }, + attributes: ['id', 'uid', 'nickname', 'avatar', 'createdAt'], + order: [['createdAt', 'DESC']], + limit: parseInt(limit), + offset: parseInt(offset), + }); + + // Get commission records for each invited user + const usersWithCommissions = await Promise.all( + invitedUsers.map(async (user) => { + // Get all commissions for this invitee + const commissions = await Commission.findAll({ + where: { + inviterId, + inviteeId: user.id, + status: 'credited' + }, + include: [ + { + model: PaymentOrder, + as: 'paymentOrder', + attributes: ['id', 'orderNo', 'serviceContent', 'paymentTime', 'amountRmb', 'amountPeso'], + }, + ], + order: [['createdAt', 'DESC']], + }); + + // Calculate totals by currency + let totalCommissionRmb = 0; + let totalCommissionPeso = 0; + let totalPaymentRmb = 0; + let totalPaymentPeso = 0; + + const orders = commissions.map(c => { + const amount = parseFloat(c.commissionAmount); + const paymentAmount = parseFloat(c.paymentAmount); + + if (c.currency === 'PHP') { + totalCommissionPeso += amount; + totalPaymentPeso += paymentAmount; + } else { + totalCommissionRmb += amount; + totalPaymentRmb += paymentAmount; + } + + return { + id: c.id, + orderNo: c.paymentOrder?.orderNo || '-', + serviceContent: c.paymentOrder?.serviceContent || '-', + paymentTime: c.paymentOrder?.paymentTime || c.createdAt, + paymentAmount: paymentAmount.toFixed(2), + commissionAmount: amount.toFixed(2), + currency: c.currency || 'RMB', + createdAt: c.createdAt, + }; + }); + + return { + user: { + id: user.id, + uid: user.uid, + nickname: user.nickname, + avatar: user.avatar, + registeredAt: user.createdAt, + }, + orderCount: orders.length, + orders, + totalPaymentRmb: totalPaymentRmb.toFixed(2), + totalPaymentPeso: totalPaymentPeso.toFixed(2), + totalCommissionRmb: totalCommissionRmb.toFixed(2), + totalCommissionPeso: totalCommissionPeso.toFixed(2), + }; + }) + ); + + return { + records: usersWithCommissions, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total: count, + totalPages: Math.ceil(count / limit), + }, + }; +}; + /** * Generate invitation code * @returns {string} Invitation code @@ -331,6 +430,7 @@ const getPlatformCommissionStats = async () => { module.exports = { calculateCommission, getCommissionsByInviter, + getInvitedUsersWithCommissions, getCommissionStats, getAllCommissions, getPlatformCommissionStats, diff --git a/miniprogram/src/locale/en.js b/miniprogram/src/locale/en.js index 35be164..e7e4be8 100644 --- a/miniprogram/src/locale/en.js +++ b/miniprogram/src/locale/en.js @@ -206,7 +206,7 @@ If you have privacy questions, please contact us through the application.` withdrawApplication: 'Withdraw Application', enterAmount: 'Please enter withdraw amount', enterPlaceholder: 'Please enter', - amountHint: 'Minimum 1 yuan per time, available 99 yuan', + amountHint: 'Minimum 1 yuan per time', nextStep: 'Next Step', selectPaymentMethod: 'Please select payment method', wechat: 'WeChat', diff --git a/miniprogram/src/locale/es.js b/miniprogram/src/locale/es.js index af57ad7..0670f96 100644 --- a/miniprogram/src/locale/es.js +++ b/miniprogram/src/locale/es.js @@ -206,7 +206,7 @@ Si tiene preguntas sobre privacidad, contáctenos a través de la aplicación.` withdrawApplication: 'Solicitud de Retiro', enterAmount: 'Por favor, ingrese el monto del retiro', enterPlaceholder: 'Por favor, ingrese', - amountHint: 'Mínimo 1 yuan por vez, disponible 99 yuan', + amountHint: 'Mínimo 1 yuan por vez', nextStep: 'Siguiente Paso', selectPaymentMethod: 'Por favor, seleccione el método de pago', wechat: 'WeChat', diff --git a/miniprogram/src/locale/zh.js b/miniprogram/src/locale/zh.js index 50e7df7..223a33a 100644 --- a/miniprogram/src/locale/zh.js +++ b/miniprogram/src/locale/zh.js @@ -222,7 +222,7 @@ export default { withdrawApplication: '提现申请', enterAmount: '请输入提现金额', enterPlaceholder: '请输入', - amountHint: '每次最低1元,待提现99元', + amountHint: '每次最低1元', nextStep: '下一步', selectPaymentMethod: '请选择收款方式', wechat: '微信', diff --git a/miniprogram/src/pages/me/invite-reward-page.vue b/miniprogram/src/pages/me/invite-reward-page.vue index 3f638a2..4c287c5 100644 --- a/miniprogram/src/pages/me/invite-reward-page.vue +++ b/miniprogram/src/pages/me/invite-reward-page.vue @@ -235,7 +235,7 @@ :placeholder="$t('invite.enterPlaceholder')" /> {{ withdrawCurrency === 'CNY' ? '¥' : '₱' }} - {{ $t('invite.amountHint') }} + {{ $t('invite.amountHint') }},{{ withdrawCurrency === 'PHP' ? '₱' : '¥' }}{{ currentAvailableBalance }} {{ $t('invite.nextStep') }} @@ -360,6 +360,13 @@ return 'overflow: hidden;' } return '' + }, + // 当前选择货币的可用余额 + currentAvailableBalance() { + if (this.withdrawCurrency === 'PHP') { + return this.commissionStats.availableBalancePeso || '0.00' + } + return this.commissionStats.availableBalanceRmb || this.commissionStats.availableBalance || '0.00' } }, onLoad() {