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 @@
+
+
+
+
邀请用户记录
+
+
+
+ {{ row.user?.nickname || '-' }}
+
+
+
+
+ {{ row.user?.uid || '-' }}
+
+
+
+
+
+ {{ row.orderCount }}
+
+
+
+
+
+
+ ¥{{ row.totalCommissionRmb }}
+ ₱{{ row.totalCommissionPeso }}
+ -
+
+
+
+
+
+ {{ formatDate(row.user?.registeredAt) }}
+
+
+
+
+
+ 查看订单
+
+
+
+
+
关闭
+
+
+
+
+
+ {{ selectedInvitedUser.user?.nickname || '-' }}
+ {{ selectedInvitedUser.user?.uid || '-' }}
+ {{ selectedInvitedUser.orderCount }}
+
+ ¥{{ selectedInvitedUser.totalCommissionRmb }}
+ ₱{{ selectedInvitedUser.totalCommissionPeso }}
+
+
+
+
订单记录
+
+
+
+
+ {{ formatDate(row.paymentTime || row.createdAt) }}
+
+
+
+
+ {{ row.currency === 'PHP' ? '₱' : '¥' }}{{ row.paymentAmount }}
+
+
+
+
+ {{ row.currency === 'PHP' ? '₱' : '¥' }}{{ row.commissionAmount }}
+
+
+
+
+
+ 关闭
+
+
@@ -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() {