邀请佣金.

This commit is contained in:
18631081161 2025-12-24 12:27:46 +08:00
parent bbaf345509
commit 2e90b3e4b2
9 changed files with 397 additions and 19 deletions

View File

@ -195,8 +195,16 @@
<span class="value">{{ userDetails.invitations.totalInvites }}</span> <span class="value">{{ userDetails.invitations.totalInvites }}</span>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<span class="label">累计奖励</span> <span class="label">已付费人数</span>
<span class="value primary">¥{{ parseFloat(userDetails.invitations.totalRewards || 0).toFixed(2) }}</span> <span class="value success">{{ userDetails.invitations.paidInvites || 0 }}</span>
</div>
<div class="stat-item">
<span class="label">累计奖励(¥)</span>
<span class="value primary">¥{{ parseFloat(userDetails.invitations.totalRewardsRmb || userDetails.invitations.totalRewards || 0).toFixed(2) }}</span>
</div>
<div class="stat-item" v-if="parseFloat(userDetails.invitations.totalRewardsPeso || 0) > 0">
<span class="label">累计奖励()</span>
<span class="value primary">{{ parseFloat(userDetails.invitations.totalRewardsPeso || 0).toFixed(2) }}</span>
</div> </div>
</div> </div>
</el-card> </el-card>
@ -247,12 +255,100 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div>
<!-- Invited Users Details -->
<div class="invited-users-section" v-if="userDetails.invitedUsers && userDetails.invitedUsers.length > 0">
<h4>邀请用户记录</h4>
<el-table :data="userDetails.invitedUsers" stripe border size="small" max-height="400">
<el-table-column prop="user.nickname" label="用户昵称" width="120">
<template #default="{ row }">
{{ row.user?.nickname || '-' }}
</template>
</el-table-column>
<el-table-column prop="user.uid" label="UID" width="100">
<template #default="{ row }">
{{ row.user?.uid || '-' }}
</template>
</el-table-column>
<el-table-column prop="orderCount" label="订单数" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.orderCount > 0 ? 'success' : 'info'" size="small">
{{ row.orderCount }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="累计奖励" width="180">
<template #default="{ row }">
<div>
<span v-if="parseFloat(row.totalCommissionRmb) > 0" class="reward-amount">¥{{ row.totalCommissionRmb }}</span>
<span v-if="parseFloat(row.totalCommissionPeso) > 0" class="reward-amount peso">{{ row.totalCommissionPeso }}</span>
<span v-if="parseFloat(row.totalCommissionRmb) === 0 && parseFloat(row.totalCommissionPeso) === 0">-</span>
</div>
</template>
</el-table-column>
<el-table-column label="注册时间" width="160">
<template #default="{ row }">
{{ formatDate(row.user?.registeredAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="showOrderDetails(row)" :disabled="row.orderCount === 0">
查看订单
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template> </template>
</div> </div>
<template #footer> <template #footer>
<el-button @click="detailsDialogVisible = false">关闭</el-button> <el-button @click="detailsDialogVisible = false">关闭</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- Order Details Dialog -->
<el-dialog
v-model="orderDetailsDialogVisible"
title="订单详情"
width="700px"
destroy-on-close
>
<div class="order-details" v-if="selectedInvitedUser">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="用户昵称">{{ selectedInvitedUser.user?.nickname || '-' }}</el-descriptions-item>
<el-descriptions-item label="UID">{{ selectedInvitedUser.user?.uid || '-' }}</el-descriptions-item>
<el-descriptions-item label="订单数">{{ selectedInvitedUser.orderCount }}</el-descriptions-item>
<el-descriptions-item label="累计奖励">
<span v-if="parseFloat(selectedInvitedUser.totalCommissionRmb) > 0">¥{{ selectedInvitedUser.totalCommissionRmb }}</span>
<span v-if="parseFloat(selectedInvitedUser.totalCommissionPeso) > 0"> {{ selectedInvitedUser.totalCommissionPeso }}</span>
</el-descriptions-item>
</el-descriptions>
<h4 style="margin: 16px 0 12px;">订单记录</h4>
<el-table :data="selectedInvitedUser.orders" stripe border size="small" max-height="300">
<el-table-column prop="orderNo" label="订单号" width="160" show-overflow-tooltip />
<el-table-column prop="paymentTime" label="支付时间" width="160">
<template #default="{ row }">
{{ formatDate(row.paymentTime || row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="支付金额" width="100" align="right">
<template #default="{ row }">
{{ row.currency === 'PHP' ? '₱' : '¥' }}{{ row.paymentAmount }}
</template>
</el-table-column>
<el-table-column label="奖励金额" width="100" align="right">
<template #default="{ row }">
<span class="reward-text">{{ row.currency === 'PHP' ? '₱' : '¥' }}{{ row.commissionAmount }}</span>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<el-button @click="orderDetailsDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
@ -286,6 +382,10 @@ const detailsDialogVisible = ref(false)
const detailsLoading = ref(false) const detailsLoading = ref(false)
const userDetails = ref(null) const userDetails = ref(null)
// Order details dialog state
const orderDetailsDialogVisible = ref(false)
const selectedInvitedUser = ref(null)
// Fetch user list // Fetch user list
async function fetchUsers() { async function fetchUsers() {
loading.value = true loading.value = true
@ -380,6 +480,12 @@ function handleViewDetails(row) {
fetchUserDetails(row.id) fetchUserDetails(row.id)
} }
// Show order details for invited user
function showOrderDetails(invitedUser) {
selectedInvitedUser.value = invitedUser
orderDetailsDialogVisible.value = true
}
// Handle toggle status // Handle toggle status
async function handleToggleStatus(row) { async function handleToggleStatus(row) {
const newStatus = row.status === 'active' ? 'suspended' : 'active' 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 { .balance {
font-weight: 600; font-weight: 600;
color: #409eff; color: #409eff;
} }
} }
.order-details {
.reward-text {
font-weight: 600;
color: #67c23a;
}
}
} }
</style> </style>

View File

@ -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 = { module.exports = {
getMyCommissions, getMyCommissions,
getMyCommissionStats, getMyCommissionStats,
getInvitedUsers,
}; };

View File

@ -22,4 +22,11 @@ router.get('/', authenticateUser, commissionController.getMyCommissions);
*/ */
router.get('/stats', authenticateUser, commissionController.getMyCommissionStats); 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; module.exports = router;

View File

@ -3,6 +3,8 @@ const Appointment = require('../models/Appointment');
const Invitation = require('../models/Invitation'); const Invitation = require('../models/Invitation');
const Withdrawal = require('../models/Withdrawal'); const Withdrawal = require('../models/Withdrawal');
const LoginHistory = require('../models/LoginHistory'); const LoginHistory = require('../models/LoginHistory');
const Commission = require('../models/Commission');
const PaymentOrder = require('../models/PaymentOrder');
const { Op } = require('sequelize'); const { Op } = require('sequelize');
const { Parser } = require('json2csv'); const { Parser } = require('json2csv');
@ -152,21 +154,118 @@ const getUserDetails = async (userId) => {
appointmentCounts.total += parseInt(stat.count); appointmentCounts.total += parseInt(stat.count);
}); });
// Get invitation statistics // Get invitation statistics from Commission table (more accurate)
const invitationStats = await Invitation.findAll({ const commissions = await Commission.findAll({
where: { inviterId: userId }, where: { inviterId: userId, status: 'credited' },
attributes: [ attributes: ['commissionAmount', 'currency'],
[require('sequelize').fn('COUNT', require('sequelize').col('id')), 'totalInvites'],
[require('sequelize').fn('SUM', require('sequelize').col('reward_amount')), 'totalRewards'],
],
raw: true, 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 = { const invitationCounts = {
totalInvites: parseInt(invitationStats[0]?.totalInvites || 0), totalInvites: invitedUsersCount,
totalRewards: parseFloat(invitationStats[0]?.totalRewards || 0), 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 // Get withdrawal statistics
const withdrawalStats = await Withdrawal.findAll({ const withdrawalStats = await Withdrawal.findAll({
where: { userId }, where: { userId },
@ -207,6 +306,7 @@ const getUserDetails = async (userId) => {
inviter, inviter,
appointments: appointmentCounts, appointments: appointmentCounts,
invitations: invitationCounts, invitations: invitationCounts,
invitedUsers: invitedUsersWithCommissions,
withdrawals: withdrawalCounts, withdrawals: withdrawalCounts,
loginHistory, loginHistory,
}; };

View File

@ -125,19 +125,20 @@ const getCommissionsByInviter = async (inviterId, options = {}) => {
const records = rows.map(commission => ({ const records = rows.map(commission => ({
id: commission.id, id: commission.id,
invitee: { invitee: commission.invitee ? {
id: commission.invitee.id, id: commission.invitee.id,
uid: commission.invitee.uid, uid: commission.invitee.uid,
nickname: commission.invitee.nickname, nickname: commission.invitee.nickname,
avatar: commission.invitee.avatar, avatar: commission.invitee.avatar,
}, } : null,
paymentOrder: { paymentOrder: commission.paymentOrder ? {
id: commission.paymentOrder.id, id: commission.paymentOrder.id,
orderNo: commission.paymentOrder.orderNo, orderNo: commission.paymentOrder.orderNo,
serviceContent: commission.paymentOrder.serviceContent, serviceContent: commission.paymentOrder.serviceContent,
paymentTime: commission.paymentOrder.paymentTime, paymentTime: commission.paymentOrder.paymentTime,
}, } : null,
paymentAmount: parseFloat(commission.paymentAmount).toFixed(2), paymentAmount: parseFloat(commission.paymentAmount).toFixed(2),
currency: commission.currency || 'RMB',
commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`, commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`,
commissionAmount: parseFloat(commission.commissionAmount).toFixed(2), commissionAmount: parseFloat(commission.commissionAmount).toFixed(2),
createdAt: commission.createdAt, 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 * Generate invitation code
* @returns {string} Invitation code * @returns {string} Invitation code
@ -331,6 +430,7 @@ const getPlatformCommissionStats = async () => {
module.exports = { module.exports = {
calculateCommission, calculateCommission,
getCommissionsByInviter, getCommissionsByInviter,
getInvitedUsersWithCommissions,
getCommissionStats, getCommissionStats,
getAllCommissions, getAllCommissions,
getPlatformCommissionStats, getPlatformCommissionStats,

View File

@ -206,7 +206,7 @@ If you have privacy questions, please contact us through the application.`
withdrawApplication: 'Withdraw Application', withdrawApplication: 'Withdraw Application',
enterAmount: 'Please enter withdraw amount', enterAmount: 'Please enter withdraw amount',
enterPlaceholder: 'Please enter', enterPlaceholder: 'Please enter',
amountHint: 'Minimum 1 yuan per time, available 99 yuan', amountHint: 'Minimum 1 yuan per time',
nextStep: 'Next Step', nextStep: 'Next Step',
selectPaymentMethod: 'Please select payment method', selectPaymentMethod: 'Please select payment method',
wechat: 'WeChat', wechat: 'WeChat',

View File

@ -206,7 +206,7 @@ Si tiene preguntas sobre privacidad, contáctenos a través de la aplicación.`
withdrawApplication: 'Solicitud de Retiro', withdrawApplication: 'Solicitud de Retiro',
enterAmount: 'Por favor, ingrese el monto del retiro', enterAmount: 'Por favor, ingrese el monto del retiro',
enterPlaceholder: 'Por favor, ingrese', enterPlaceholder: 'Por favor, ingrese',
amountHint: 'Mínimo 1 yuan por vez, disponible 99 yuan', amountHint: 'Mínimo 1 yuan por vez',
nextStep: 'Siguiente Paso', nextStep: 'Siguiente Paso',
selectPaymentMethod: 'Por favor, seleccione el método de pago', selectPaymentMethod: 'Por favor, seleccione el método de pago',
wechat: 'WeChat', wechat: 'WeChat',

View File

@ -222,7 +222,7 @@ export default {
withdrawApplication: '提现申请', withdrawApplication: '提现申请',
enterAmount: '请输入提现金额', enterAmount: '请输入提现金额',
enterPlaceholder: '请输入', enterPlaceholder: '请输入',
amountHint: '每次最低1元待提现99元', amountHint: '每次最低1元',
nextStep: '下一步', nextStep: '下一步',
selectPaymentMethod: '请选择收款方式', selectPaymentMethod: '请选择收款方式',
wechat: '微信', wechat: '微信',

View File

@ -235,7 +235,7 @@
:placeholder="$t('invite.enterPlaceholder')" /> :placeholder="$t('invite.enterPlaceholder')" />
<text class="currency-text">{{ withdrawCurrency === 'CNY' ? '¥' : '₱' }}</text> <text class="currency-text">{{ withdrawCurrency === 'CNY' ? '¥' : '₱' }}</text>
</view> </view>
<text class="amount-hint">{{ $t('invite.amountHint') }}</text> <text class="amount-hint">{{ $t('invite.amountHint') }}{{ withdrawCurrency === 'PHP' ? '₱' : '¥' }}{{ currentAvailableBalance }}</text>
<view class="apply-btn" @click="nextStep"> <view class="apply-btn" @click="nextStep">
<text class="apply-btn-text">{{ $t('invite.nextStep') }}</text> <text class="apply-btn-text">{{ $t('invite.nextStep') }}</text>
@ -360,6 +360,13 @@
return 'overflow: hidden;' return 'overflow: hidden;'
} }
return '' return ''
},
//
currentAvailableBalance() {
if (this.withdrawCurrency === 'PHP') {
return this.commissionStats.availableBalancePeso || '0.00'
}
return this.commissionStats.availableBalanceRmb || this.commissionStats.availableBalance || '0.00'
} }
}, },
onLoad() { onLoad() {