邀请佣金.

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>
</div>
<div class="stat-item">
<span class="label">累计奖励</span>
<span class="value primary">¥{{ parseFloat(userDetails.invitations.totalRewards || 0).toFixed(2) }}</span>
<span class="label">已付费人数</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>
</el-card>
@ -247,12 +255,100 @@
</el-table-column>
</el-table>
</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>
</div>
<template #footer>
<el-button @click="detailsDialogVisible = false">关闭</el-button>
</template>
</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>
</template>
@ -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;
}
}
}
</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 = {
getMyCommissions,
getMyCommissionStats,
getInvitedUsers,
};

View File

@ -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;

View File

@ -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,
};

View File

@ -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,

View File

@ -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',

View File

@ -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',

View File

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

View File

@ -235,7 +235,7 @@
:placeholder="$t('invite.enterPlaceholder')" />
<text class="currency-text">{{ withdrawCurrency === 'CNY' ? '¥' : '₱' }}</text>
</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">
<text class="apply-btn-text">{{ $t('invite.nextStep') }}</text>
@ -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() {