逻辑修改
This commit is contained in:
parent
7892907809
commit
5981038a0f
|
|
@ -1,37 +1,60 @@
|
|||
<template>
|
||||
<div class="commissions-container">
|
||||
<!-- Statistics Cards -->
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-row :gutter="16" class="stats-row">
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<el-card class="stat-card rmb-card" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value rmb">¥{{ stats.totalCommissionPaidRmb || stats.totalCommissionPaid }}</div>
|
||||
<div class="stat-label">累计佣金支出(人民币)</div>
|
||||
<div class="stat-icon">
|
||||
<el-icon><Money /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">¥{{ stats.totalCommissionPaidRmb || stats.totalCommissionPaid }}</div>
|
||||
<div class="stat-label">累计佣金支出(人民币)</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<el-card class="stat-card peso-card" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value peso">₱{{ stats.totalCommissionPaidPeso || '0.00' }}</div>
|
||||
<div class="stat-label">累计佣金支出(比索)</div>
|
||||
<div class="stat-icon">
|
||||
<el-icon><Coin /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">₱{{ stats.totalCommissionPaidPeso || '0.00' }}</div>
|
||||
<div class="stat-label">累计佣金支出(比索)</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<el-card class="stat-card count-card" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.totalCommissionCount }}</div>
|
||||
<div class="stat-label">佣金记录数</div>
|
||||
<div class="stat-icon">
|
||||
<el-icon><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.totalCommissionCount }}</div>
|
||||
<div class="stat-label">佣金记录数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<el-card class="stat-card pending-card" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value rmb">¥{{ stats.pendingWithdrawalRmb || stats.pendingWithdrawal }}</div>
|
||||
<div class="stat-value peso">₱{{ stats.pendingWithdrawalPeso || '0.00' }}</div>
|
||||
<div class="stat-label">待提现金额</div>
|
||||
<div class="stat-icon">
|
||||
<el-icon><Wallet /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value dual">
|
||||
<span class="rmb">¥{{ stats.pendingWithdrawalRmb || stats.pendingWithdrawal }}</span>
|
||||
<span class="divider">/</span>
|
||||
<span class="peso">₱{{ stats.pendingWithdrawalPeso || '0.00' }}</span>
|
||||
</div>
|
||||
<div class="stat-label">待提现金额</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
|
@ -44,13 +67,106 @@
|
|||
<span>佣金比例配置</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :inline="true">
|
||||
<el-form-item label="当前佣金比例">
|
||||
<el-tag type="primary" size="large">{{ rateConfig.percentage }}</el-tag>
|
||||
|
||||
<!-- 系统默认比例 -->
|
||||
<div class="config-section">
|
||||
<h4>系统默认比例</h4>
|
||||
<el-form :inline="true">
|
||||
<el-form-item label="当前佣金比例">
|
||||
<el-tag type="primary" size="large">{{ rateConfig.percentage }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="设置新比例">
|
||||
<el-input-number
|
||||
v-model="newRate"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
:step="0.5"
|
||||
/>
|
||||
<span class="rate-suffix">%</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="savingRate" @click="handleSaveRate">保存</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<!-- 用户专属比例 -->
|
||||
<div class="config-section">
|
||||
<h4>用户专属比例设置</h4>
|
||||
<p class="section-desc">为特定用户设置专属佣金比例,未设置的用户将使用系统默认比例</p>
|
||||
|
||||
<!-- 添加用户专属比例 -->
|
||||
<el-form :inline="true" class="user-rate-form">
|
||||
<el-form-item label="用户UID">
|
||||
<el-input v-model="userRateForm.uid" placeholder="输入用户UID" style="width: 120px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="佣金比例">
|
||||
<el-input-number
|
||||
v-model="userRateForm.rate"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
:step="0.5"
|
||||
/>
|
||||
<span class="rate-suffix">%</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="success" :loading="savingUserRate" @click="handleAddUserRate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 已设置专属比例的用户列表 -->
|
||||
<el-table :data="usersWithCustomRates" stripe border size="small" style="margin-top: 16px">
|
||||
<el-table-column prop="uid" label="UID" width="100" />
|
||||
<el-table-column prop="nickname" label="昵称" width="150" />
|
||||
<el-table-column prop="percentage" label="专属比例" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="success">{{ row.percentage }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleEditUserRate(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
修改
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleRemoveUserRate(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
移除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="usersWithCustomRates.length === 0" class="empty-tip">
|
||||
暂无用户设置专属比例
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-hint">
|
||||
<el-alert
|
||||
title="注意:修改佣金比例仅对新创建的支付订单生效,不影响已计入的历史佣金记录。用户专属比例优先于系统默认比例。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Edit User Rate Dialog -->
|
||||
<el-dialog v-model="editUserRateDialogVisible" title="修改用户佣金比例" width="400px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="用户">
|
||||
<span>{{ editingUser?.nickname }} (UID: {{ editingUser?.uid }})</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="设置新比例">
|
||||
<el-form-item label="佣金比例">
|
||||
<el-input-number
|
||||
v-model="newRate"
|
||||
v-model="editUserRateValue"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
|
|
@ -58,19 +174,12 @@
|
|||
/>
|
||||
<span class="rate-suffix">%</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="savingRate" @click="handleSaveRate">保存</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="config-hint">
|
||||
<el-alert
|
||||
title="注意:修改佣金比例仅对新创建的支付订单生效,不影响已计入的历史佣金记录。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
<template #footer>
|
||||
<el-button @click="editUserRateDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="savingUserRate" @click="handleSaveEditUserRate">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Commissions Table -->
|
||||
<el-card class="table-card">
|
||||
|
|
@ -150,14 +259,25 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Edit, Delete, Money, Coin, Document, Wallet } from '@element-plus/icons-vue'
|
||||
import api from '@/utils/api'
|
||||
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const savingRate = ref(false)
|
||||
const savingUserRate = ref(false)
|
||||
const commissions = ref([])
|
||||
const newRate = ref(2)
|
||||
const usersWithCustomRates = ref([])
|
||||
const editUserRateDialogVisible = ref(false)
|
||||
const editingUser = ref(null)
|
||||
const editUserRateValue = ref(0)
|
||||
|
||||
const userRateForm = reactive({
|
||||
uid: '',
|
||||
rate: 5,
|
||||
})
|
||||
|
||||
const stats = reactive({
|
||||
totalCommissionPaid: '0.00',
|
||||
|
|
@ -201,6 +321,17 @@ async function fetchRateConfig() {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchUsersWithCustomRates() {
|
||||
try {
|
||||
const response = await api.get('/api/v1/admin/commissions/config/user-commission-rates')
|
||||
if (response.data.code === 0) {
|
||||
usersWithCustomRates.value = response.data.data.users || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch users with custom rates:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCommissions() {
|
||||
loading.value = true
|
||||
try {
|
||||
|
|
@ -248,6 +379,114 @@ async function handleSaveRate() {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleAddUserRate() {
|
||||
if (!userRateForm.uid) {
|
||||
ElMessage.error('请输入用户UID')
|
||||
return
|
||||
}
|
||||
if (userRateForm.rate < 0 || userRateForm.rate > 100) {
|
||||
ElMessage.error('佣金比例必须在 0-100% 之间')
|
||||
return
|
||||
}
|
||||
|
||||
savingUserRate.value = true
|
||||
try {
|
||||
// First find user by UID
|
||||
const userResponse = await api.get('/api/v1/admin/users', {
|
||||
params: { search: userRateForm.uid, limit: 1 }
|
||||
})
|
||||
|
||||
if (userResponse.data.code !== 0 || !userResponse.data.data.users?.length) {
|
||||
ElMessage.error('未找到该用户')
|
||||
return
|
||||
}
|
||||
|
||||
const user = userResponse.data.data.users[0]
|
||||
|
||||
// Set user commission rate
|
||||
const response = await api.put(`/api/v1/admin/commissions/config/user-commission-rate/${user.id}`, {
|
||||
rate: userRateForm.rate / 100,
|
||||
})
|
||||
|
||||
if (response.data.code === 0) {
|
||||
ElMessage.success('用户专属比例设置成功')
|
||||
userRateForm.uid = ''
|
||||
userRateForm.rate = 5
|
||||
fetchUsersWithCustomRates()
|
||||
} else {
|
||||
ElMessage.error(response.data.error?.message || '设置失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add user rate:', error)
|
||||
ElMessage.error(error.response?.data?.error?.message || '设置失败')
|
||||
} finally {
|
||||
savingUserRate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditUserRate(row) {
|
||||
editingUser.value = row
|
||||
editUserRateValue.value = row.commissionRate * 100
|
||||
editUserRateDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSaveEditUserRate() {
|
||||
if (editUserRateValue.value < 0 || editUserRateValue.value > 100) {
|
||||
ElMessage.error('佣金比例必须在 0-100% 之间')
|
||||
return
|
||||
}
|
||||
|
||||
savingUserRate.value = true
|
||||
try {
|
||||
const response = await api.put(`/api/v1/admin/commissions/config/user-commission-rate/${editingUser.value.userId}`, {
|
||||
rate: editUserRateValue.value / 100,
|
||||
})
|
||||
|
||||
if (response.data.code === 0) {
|
||||
ElMessage.success('用户专属比例已更新')
|
||||
editUserRateDialogVisible.value = false
|
||||
fetchUsersWithCustomRates()
|
||||
} else {
|
||||
ElMessage.error(response.data.error?.message || '更新失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save user rate:', error)
|
||||
ElMessage.error('更新失败')
|
||||
} finally {
|
||||
savingUserRate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoveUserRate(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要移除用户 "${row.nickname}" (UID: ${row.uid}) 的专属佣金比例吗?移除后将使用系统默认比例。`,
|
||||
'确认移除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
|
||||
const response = await api.put(`/api/v1/admin/commissions/config/user-commission-rate/${row.userId}`, {
|
||||
rate: null,
|
||||
})
|
||||
|
||||
if (response.data.code === 0) {
|
||||
ElMessage.success('已移除用户专属比例')
|
||||
fetchUsersWithCustomRates()
|
||||
} else {
|
||||
ElMessage.error(response.data.error?.message || '移除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('Failed to remove user rate:', error)
|
||||
ElMessage.error('移除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(page) {
|
||||
pagination.page = page
|
||||
fetchCommissions()
|
||||
|
|
@ -268,6 +507,7 @@ function formatDate(date) {
|
|||
onMounted(() => {
|
||||
fetchStats()
|
||||
fetchRateConfig()
|
||||
fetchUsersWithCustomRates()
|
||||
fetchCommissions()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -279,35 +519,145 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
.stat-card {
|
||||
.stat-content {
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
|
||||
&.rmb {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
&.peso {
|
||||
color: #67c23a;
|
||||
font-size: 20px;
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.el-icon {
|
||||
font-size: 28px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&.dual {
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
|
||||
.rmb {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.peso {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: #c0c4cc;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.rmb-card {
|
||||
.stat-icon {
|
||||
background: linear-gradient(135deg, #f6d365 0%, #e6a23c 100%);
|
||||
}
|
||||
.stat-value {
|
||||
color: #e6a23c;
|
||||
}
|
||||
}
|
||||
|
||||
&.peso-card {
|
||||
.stat-icon {
|
||||
background: linear-gradient(135deg, #84fab0 0%, #67c23a 100%);
|
||||
}
|
||||
.stat-value {
|
||||
color: #67c23a;
|
||||
}
|
||||
}
|
||||
|
||||
&.count-card {
|
||||
.stat-icon {
|
||||
background: linear-gradient(135deg, #667eea 0%, #409eff 100%);
|
||||
}
|
||||
.stat-value {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
&.pending-card {
|
||||
.stat-icon {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.config-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 12px;
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin: -8px 0 16px 0;
|
||||
}
|
||||
|
||||
.user-rate-form {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 30px;
|
||||
font-size: 14px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.rate-suffix {
|
||||
margin-left: 8px;
|
||||
|
|
@ -321,23 +671,23 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
.table-card {
|
||||
border-radius: 12px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.payment-amount {
|
||||
color: #e6a23c;
|
||||
|
||||
&.peso {
|
||||
color: #67c23a;
|
||||
}
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.commission-amount {
|
||||
color: #67c23a;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@
|
|||
</el-card>
|
||||
|
||||
<!-- Create Dialog -->
|
||||
<el-dialog v-model="createDialogVisible" title="创建支付订单" width="500px">
|
||||
<el-dialog v-model="createDialogVisible" title="创建支付订单" width="550px">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
|
||||
<el-form-item label="用户ID" prop="userId">
|
||||
<el-input v-model="createForm.userId" placeholder="输入用户UID(6位数字)" />
|
||||
|
|
@ -157,6 +157,22 @@
|
|||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="支付凭证" prop="paymentProof" required>
|
||||
<el-upload
|
||||
class="payment-proof-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:before-upload="beforeUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img v-if="createForm.paymentProof" :src="getImageUrl(createForm.paymentProof)" class="payment-proof-image" />
|
||||
<el-icon v-else class="payment-proof-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<div class="upload-tip">点击上传支付凭证截图(必填)</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联预约单">
|
||||
<el-input v-model="createForm.appointmentId" placeholder="可选,输入预约单ID" />
|
||||
</el-form-item>
|
||||
|
|
@ -190,6 +206,16 @@
|
|||
<el-descriptions-item label="服务内容" :span="2">{{ currentOrder.serviceContent }}</el-descriptions-item>
|
||||
<el-descriptions-item label="支付时间">{{ formatDate(currentOrder.paymentTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(currentOrder.createdAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="支付凭证" :span="2">
|
||||
<el-image
|
||||
v-if="currentOrder.paymentProof"
|
||||
:src="getImageUrl(currentOrder.paymentProof)"
|
||||
:preview-src-list="[getImageUrl(currentOrder.paymentProof)]"
|
||||
fit="contain"
|
||||
style="width: 120px; height: 120px;"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ currentOrder.notes || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人">{{ currentOrder.creator?.realName || currentOrder.creator?.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="关联预约单">{{ currentOrder.appointment?.appointmentNo || '-' }}</el-descriptions-item>
|
||||
|
|
@ -232,8 +258,9 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import api from '@/utils/api'
|
||||
|
||||
// State
|
||||
|
|
@ -246,6 +273,19 @@ const detailsDialogVisible = ref(false)
|
|||
const currentOrder = ref(null)
|
||||
const createFormRef = ref(null)
|
||||
|
||||
// Upload config
|
||||
const uploadUrl = computed(() => {
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || ''
|
||||
return `${baseUrl}/api/v1/admin/upload/image`
|
||||
})
|
||||
|
||||
const uploadHeaders = computed(() => {
|
||||
const token = localStorage.getItem('admin_token')
|
||||
return {
|
||||
Authorization: token ? `Bearer ${token}` : ''
|
||||
}
|
||||
})
|
||||
|
||||
const filters = reactive({
|
||||
userId: '',
|
||||
status: '',
|
||||
|
|
@ -269,6 +309,7 @@ const createForm = reactive({
|
|||
amountRmb: null,
|
||||
serviceContent: '',
|
||||
paymentTime: '',
|
||||
paymentProof: '',
|
||||
appointmentId: '',
|
||||
notes: '',
|
||||
})
|
||||
|
|
@ -277,6 +318,7 @@ const createRules = {
|
|||
userId: [{ required: true, message: '请输入用户ID', trigger: 'blur' }],
|
||||
serviceContent: [{ required: true, message: '请输入服务内容', trigger: 'blur' }],
|
||||
paymentTime: [{ required: true, message: '请选择支付时间', trigger: 'change' }],
|
||||
paymentProof: [{ required: true, message: '请上传支付凭证', trigger: 'change' }],
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
|
@ -359,12 +401,50 @@ function showCreateDialog() {
|
|||
amountRmb: null,
|
||||
serviceContent: '',
|
||||
paymentTime: currentTime,
|
||||
paymentProof: '',
|
||||
appointmentId: '',
|
||||
notes: '',
|
||||
})
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
// Upload handlers
|
||||
function getImageUrl(path) {
|
||||
if (!path) return ''
|
||||
if (path.startsWith('http')) return path
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || ''
|
||||
return `${baseUrl}${path}`
|
||||
}
|
||||
|
||||
function handleUploadSuccess(response) {
|
||||
if (response.code === 0 && response.data?.url) {
|
||||
createForm.paymentProof = response.data.url
|
||||
ElMessage.success('上传成功')
|
||||
} else {
|
||||
ElMessage.error(response.error?.message || '上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleUploadError(error) {
|
||||
console.error('Upload error:', error)
|
||||
ElMessage.error('上传失败,请重试')
|
||||
}
|
||||
|
||||
function beforeUpload(file) {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt5M = file.size / 1024 / 1024 < 5
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件')
|
||||
return false
|
||||
}
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('图片大小不能超过 5MB')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
if (!createFormRef.value) return
|
||||
|
||||
|
|
@ -376,6 +456,11 @@ async function handleCreate() {
|
|||
ElMessage.warning('请至少填写一种货币金额(比索或人民币)')
|
||||
return
|
||||
}
|
||||
|
||||
if (!createForm.paymentProof) {
|
||||
ElMessage.warning('请上传支付凭证图片')
|
||||
return
|
||||
}
|
||||
|
||||
await createFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
|
@ -388,6 +473,7 @@ async function handleCreate() {
|
|||
amountRmb: hasRmb ? createForm.amountRmb : undefined,
|
||||
serviceContent: createForm.serviceContent,
|
||||
paymentTime: createForm.paymentTime,
|
||||
paymentProof: createForm.paymentProof,
|
||||
appointmentId: createForm.appointmentId || undefined,
|
||||
notes: createForm.notes || undefined,
|
||||
})
|
||||
|
|
@ -618,5 +704,42 @@ onMounted(() => {
|
|||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.payment-proof-uploader {
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.3s;
|
||||
width: 148px;
|
||||
height: 148px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.payment-proof-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.payment-proof-image {
|
||||
width: 146px;
|
||||
height: 146px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -178,6 +178,22 @@
|
|||
<el-descriptions-item label="拒绝原因" :span="2" v-if="withdrawalDetails.rejectionReason">
|
||||
<span class="rejection-reason">{{ withdrawalDetails.rejectionReason }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付凭证" :span="2" v-if="withdrawalDetails.paymentProof">
|
||||
<el-image
|
||||
:src="getImageUrl(withdrawalDetails.paymentProof)"
|
||||
:preview-src-list="[getImageUrl(withdrawalDetails.paymentProof)]"
|
||||
fit="contain"
|
||||
style="width: 200px; height: 200px; cursor: pointer;"
|
||||
:preview-teleported="true"
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>加载失败</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- User Info -->
|
||||
|
|
@ -216,7 +232,7 @@
|
|||
<el-dialog
|
||||
v-model="approveDialogVisible"
|
||||
title="批准提现"
|
||||
width="450px"
|
||||
width="500px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="approve-dialog-content">
|
||||
|
|
@ -234,7 +250,23 @@
|
|||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-form :model="approveForm" label-width="80px" class="mt-20">
|
||||
<el-form :model="approveForm" label-width="100px" class="mt-20">
|
||||
<el-form-item label="支付凭证" required>
|
||||
<el-upload
|
||||
class="payment-proof-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleApproveUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:before-upload="beforeUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img v-if="approveForm.paymentProof" :src="getImageUrl(approveForm.paymentProof)" class="payment-proof-image" />
|
||||
<el-icon v-else class="payment-proof-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<div class="upload-tip">点击上传支付凭证截图(必填)</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="approveForm.notes"
|
||||
|
|
@ -298,9 +330,9 @@
|
|||
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Picture } from '@element-plus/icons-vue'
|
||||
import { Picture, Plus } from '@element-plus/icons-vue'
|
||||
import api from '@/utils/api'
|
||||
|
||||
// State
|
||||
|
|
@ -321,6 +353,19 @@ const sortConfig = reactive({
|
|||
sortOrder: 'DESC'
|
||||
})
|
||||
|
||||
// Upload config
|
||||
const uploadUrl = computed(() => {
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || ''
|
||||
return `${baseUrl}/api/v1/admin/upload/image`
|
||||
})
|
||||
|
||||
const uploadHeaders = computed(() => {
|
||||
const token = localStorage.getItem('admin_token')
|
||||
return {
|
||||
Authorization: token ? `Bearer ${token}` : ''
|
||||
}
|
||||
})
|
||||
|
||||
// Details dialog state
|
||||
const detailsDialogVisible = ref(false)
|
||||
const detailsLoading = ref(false)
|
||||
|
|
@ -335,7 +380,8 @@ const approveForm = reactive({
|
|||
amount: '',
|
||||
currency: 'CNY',
|
||||
userName: '',
|
||||
notes: ''
|
||||
notes: '',
|
||||
paymentProof: ''
|
||||
})
|
||||
|
||||
// Reject dialog state
|
||||
|
|
@ -458,6 +504,7 @@ function handleApprove(row) {
|
|||
approveForm.currency = row.currency || 'CNY'
|
||||
approveForm.userName = row.user?.nickname || row.user?.realName || '-'
|
||||
approveForm.notes = ''
|
||||
approveForm.paymentProof = ''
|
||||
approveDialogVisible.value = true
|
||||
}
|
||||
|
||||
|
|
@ -470,10 +517,16 @@ function handleApproveFromDetails() {
|
|||
|
||||
// Confirm approve
|
||||
async function confirmApprove() {
|
||||
if (!approveForm.paymentProof) {
|
||||
ElMessage.warning('请上传支付凭证图片')
|
||||
return
|
||||
}
|
||||
|
||||
approving.value = true
|
||||
try {
|
||||
const response = await api.put(`/api/v1/admin/withdrawals/${approveForm.withdrawalId}/approve`, {
|
||||
notes: approveForm.notes
|
||||
notes: approveForm.notes,
|
||||
paymentProof: approveForm.paymentProof
|
||||
})
|
||||
|
||||
if (response.data.code === 0) {
|
||||
|
|
@ -490,6 +543,43 @@ async function confirmApprove() {
|
|||
}
|
||||
}
|
||||
|
||||
// Upload handlers
|
||||
function getImageUrl(path) {
|
||||
if (!path) return ''
|
||||
if (path.startsWith('http')) return path
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || ''
|
||||
return `${baseUrl}${path}`
|
||||
}
|
||||
|
||||
function handleApproveUploadSuccess(response) {
|
||||
if (response.code === 0 && response.data?.url) {
|
||||
approveForm.paymentProof = response.data.url
|
||||
ElMessage.success('上传成功')
|
||||
} else {
|
||||
ElMessage.error(response.error?.message || '上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleUploadError(error) {
|
||||
console.error('Upload error:', error)
|
||||
ElMessage.error('上传失败,请重试')
|
||||
}
|
||||
|
||||
function beforeUpload(file) {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt5M = file.size / 1024 / 1024 < 5
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件')
|
||||
return false
|
||||
}
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('图片大小不能超过 5MB')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle reject
|
||||
function handleReject(row) {
|
||||
rejectForm.withdrawalId = row.id
|
||||
|
|
@ -769,6 +859,43 @@ onMounted(() => {
|
|||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.payment-proof-uploader {
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.3s;
|
||||
width: 148px;
|
||||
height: 148px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.payment-proof-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.payment-proof-image {
|
||||
width: 146px;
|
||||
height: 146px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
41
backend/add-commission-rate-field.js
Normal file
41
backend/add-commission-rate-field.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* 添加用户佣金比例字段
|
||||
* 运行: node add-commission-rate-field.js
|
||||
*/
|
||||
|
||||
const { sequelize } = require('./src/config/database');
|
||||
|
||||
async function addCommissionRateField() {
|
||||
try {
|
||||
console.log('Connecting to database...');
|
||||
await sequelize.authenticate();
|
||||
console.log('Database connected.');
|
||||
|
||||
// Check if column exists
|
||||
const [results] = await sequelize.query(`
|
||||
SELECT COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_NAME = 'user' AND COLUMN_NAME = 'commission_rate'
|
||||
`);
|
||||
|
||||
if (results.length > 0) {
|
||||
console.log('Column commission_rate already exists.');
|
||||
} else {
|
||||
console.log('Adding commission_rate column...');
|
||||
await sequelize.query(`
|
||||
ALTER TABLE user
|
||||
ADD COLUMN commission_rate DECIMAL(5,4) NULL
|
||||
COMMENT '用户专属佣金比例(如0.05表示5%),为空则使用系统默认比例'
|
||||
`);
|
||||
console.log('Column commission_rate added successfully.');
|
||||
}
|
||||
|
||||
console.log('Done!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
addCommissionRateField();
|
||||
39
backend/add-payment-proof-field.js
Normal file
39
backend/add-payment-proof-field.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Migration script to add payment_proof field to payment_orders table
|
||||
*/
|
||||
const { sequelize } = require('./src/config/database');
|
||||
|
||||
async function addPaymentProofField() {
|
||||
try {
|
||||
console.log('Connecting to database...');
|
||||
await sequelize.authenticate();
|
||||
console.log('Database connected.');
|
||||
|
||||
// Check if column already exists
|
||||
const [results] = await sequelize.query(`
|
||||
SELECT COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_NAME = 'payment_orders' AND COLUMN_NAME = 'payment_proof'
|
||||
`);
|
||||
|
||||
if (results.length > 0) {
|
||||
console.log('Column payment_proof already exists in payment_orders table.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Add the column
|
||||
console.log('Adding payment_proof column to payment_orders table...');
|
||||
await sequelize.query(`
|
||||
ALTER TABLE payment_orders
|
||||
ADD COLUMN payment_proof VARCHAR(500) NULL COMMENT '支付凭证图片URL'
|
||||
`);
|
||||
|
||||
console.log('Successfully added payment_proof column to payment_orders table.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
addPaymentProofField();
|
||||
39
backend/add-withdrawal-payment-proof-field.js
Normal file
39
backend/add-withdrawal-payment-proof-field.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Migration script to add payment_proof field to withdrawal table
|
||||
*/
|
||||
const { sequelize } = require('./src/config/database');
|
||||
|
||||
async function addPaymentProofField() {
|
||||
try {
|
||||
console.log('Connecting to database...');
|
||||
await sequelize.authenticate();
|
||||
console.log('Database connected.');
|
||||
|
||||
// Check if column already exists
|
||||
const [results] = await sequelize.query(`
|
||||
SELECT COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_NAME = 'withdrawal' AND COLUMN_NAME = 'payment_proof'
|
||||
`);
|
||||
|
||||
if (results.length > 0) {
|
||||
console.log('Column payment_proof already exists in withdrawal table.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Add the column
|
||||
console.log('Adding payment_proof column to withdrawal table...');
|
||||
await sequelize.query(`
|
||||
ALTER TABLE withdrawal
|
||||
ADD COLUMN payment_proof VARCHAR(500) NULL COMMENT '支付凭证图片URL(管理员批准时上传)'
|
||||
`);
|
||||
|
||||
console.log('Successfully added payment_proof column to withdrawal table.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
addPaymentProofField();
|
||||
|
|
@ -143,9 +143,156 @@ const setCommissionRate = async (req, res) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user's commission rate
|
||||
* GET /api/v1/admin/config/user-commission-rate/:userId
|
||||
*/
|
||||
const getUserCommissionRate = async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const rate = await commissionConfigService.getUserCommissionRate(userId);
|
||||
const systemRate = await commissionConfigService.getCommissionRate();
|
||||
|
||||
// Get user info
|
||||
const User = require('../models/User');
|
||||
const user = await User.findByPk(userId, {
|
||||
attributes: ['id', 'uid', 'nickname', 'commissionRate'],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
success: false,
|
||||
error: {
|
||||
code: 'USER_NOT_FOUND',
|
||||
message: 'User not found',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const hasCustomRate = user.commissionRate !== null && user.commissionRate !== undefined;
|
||||
|
||||
return res.status(200).json({
|
||||
code: 0,
|
||||
success: true,
|
||||
data: {
|
||||
userId: user.id,
|
||||
uid: user.uid,
|
||||
nickname: user.nickname,
|
||||
commissionRate: rate,
|
||||
percentage: `${(rate * 100).toFixed(2)}%`,
|
||||
hasCustomRate,
|
||||
customRate: hasCustomRate ? parseFloat(user.commissionRate) : null,
|
||||
systemRate,
|
||||
systemPercentage: `${(systemRate * 100).toFixed(2)}%`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
code: 500,
|
||||
success: false,
|
||||
error: {
|
||||
code: 'GET_USER_RATE_ERROR',
|
||||
message: 'Failed to get user commission rate',
|
||||
details: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set user's commission rate
|
||||
* PUT /api/v1/admin/config/user-commission-rate/:userId
|
||||
*/
|
||||
const setUserCommissionRate = async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { rate } = req.body;
|
||||
|
||||
// rate can be null to clear custom rate
|
||||
const result = await commissionConfigService.setUserCommissionRate(userId, rate);
|
||||
|
||||
return res.status(200).json({
|
||||
code: 0,
|
||||
success: true,
|
||||
data: result,
|
||||
message: rate === null || rate === ''
|
||||
? 'User commission rate cleared, will use system default'
|
||||
: 'User commission rate updated successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message === 'User not found') {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
success: false,
|
||||
error: {
|
||||
code: 'USER_NOT_FOUND',
|
||||
message: 'User not found',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('Invalid commission rate')) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_RATE',
|
||||
message: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
code: 500,
|
||||
success: false,
|
||||
error: {
|
||||
code: 'SET_USER_RATE_ERROR',
|
||||
message: 'Failed to set user commission rate',
|
||||
details: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all users with custom commission rates
|
||||
* GET /api/v1/admin/config/user-commission-rates
|
||||
*/
|
||||
const getUsersWithCustomRates = async (req, res) => {
|
||||
try {
|
||||
const users = await commissionConfigService.getUsersWithCustomRates();
|
||||
const systemRate = await commissionConfigService.getCommissionRate();
|
||||
|
||||
return res.status(200).json({
|
||||
code: 0,
|
||||
success: true,
|
||||
data: {
|
||||
systemRate,
|
||||
systemPercentage: `${(systemRate * 100).toFixed(2)}%`,
|
||||
users,
|
||||
total: users.length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
code: 500,
|
||||
success: false,
|
||||
error: {
|
||||
code: 'GET_CUSTOM_RATES_ERROR',
|
||||
message: 'Failed to get users with custom rates',
|
||||
details: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getCommissions,
|
||||
getCommissionStats,
|
||||
getCommissionRate,
|
||||
setCommissionRate,
|
||||
getUserCommissionRate,
|
||||
setUserCommissionRate,
|
||||
getUsersWithCustomRates,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const paymentOrderService = require('../services/paymentOrderService');
|
|||
const createPaymentOrder = async (req, res) => {
|
||||
try {
|
||||
const adminId = req.adminId || req.admin?.id;
|
||||
const { userId, appointmentId, amount, amountPeso, amountRmb, serviceContent, paymentTime, notes } = req.body;
|
||||
const { userId, appointmentId, amount, amountPeso, amountRmb, serviceContent, paymentTime, notes, paymentProof } = req.body;
|
||||
|
||||
const result = await paymentOrderService.createPaymentOrder({
|
||||
userId: userId ? userId.trim() : userId, // Trim whitespace
|
||||
|
|
@ -23,6 +23,7 @@ const createPaymentOrder = async (req, res) => {
|
|||
serviceContent,
|
||||
paymentTime,
|
||||
notes,
|
||||
paymentProof,
|
||||
}, adminId);
|
||||
|
||||
return res.status(201).json({
|
||||
|
|
|
|||
|
|
@ -82,12 +82,23 @@ const approveWithdrawal = async (req, res) => {
|
|||
try {
|
||||
const { id } = req.params;
|
||||
const adminId = req.admin.userId;
|
||||
const { notes } = req.body;
|
||||
const { notes, paymentProof } = req.body;
|
||||
|
||||
if (!paymentProof) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'PAYMENT_PROOF_REQUIRED',
|
||||
message: '请上传支付凭证图片',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const withdrawal = await adminWithdrawalService.approveWithdrawal(
|
||||
id,
|
||||
adminId,
|
||||
notes
|
||||
notes,
|
||||
paymentProof
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
|
|
|
|||
|
|
@ -97,6 +97,12 @@ const PaymentOrder = sequelize.define('PaymentOrder', {
|
|||
field: 'cancel_reason',
|
||||
comment: '取消原因',
|
||||
},
|
||||
paymentProof: {
|
||||
type: DataTypes.STRING(500),
|
||||
allowNull: true,
|
||||
field: 'payment_proof',
|
||||
comment: '支付凭证图片URL',
|
||||
},
|
||||
}, {
|
||||
tableName: 'payment_orders',
|
||||
timestamps: true,
|
||||
|
|
|
|||
|
|
@ -92,6 +92,12 @@ const User = sequelize.define('User', {
|
|||
defaultValue: 'active',
|
||||
allowNull: false,
|
||||
},
|
||||
commissionRate: {
|
||||
type: DataTypes.DECIMAL(5, 4),
|
||||
allowNull: true,
|
||||
field: 'commission_rate',
|
||||
comment: '用户专属佣金比例(如0.05表示5%),为空则使用系统默认比例',
|
||||
},
|
||||
}, {
|
||||
tableName: 'user',
|
||||
timestamps: true,
|
||||
|
|
|
|||
|
|
@ -75,6 +75,12 @@ const Withdrawal = sequelize.define('Withdrawal', {
|
|||
allowNull: true,
|
||||
field: 'completed_at',
|
||||
},
|
||||
paymentProof: {
|
||||
type: DataTypes.STRING(500),
|
||||
allowNull: true,
|
||||
field: 'payment_proof',
|
||||
comment: '支付凭证图片URL(管理员批准时上传)',
|
||||
},
|
||||
}, {
|
||||
tableName: 'withdrawal',
|
||||
timestamps: true,
|
||||
|
|
|
|||
|
|
@ -59,4 +59,41 @@ router.put(
|
|||
adminCommissionController.setCommissionRate
|
||||
);
|
||||
|
||||
/**
|
||||
* @route GET /api/v1/admin/config/user-commission-rates
|
||||
* @desc Get all users with custom commission rates
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.get(
|
||||
'/config/user-commission-rates',
|
||||
authenticateAdmin,
|
||||
requireRole(['super_admin', 'admin']),
|
||||
adminCommissionController.getUsersWithCustomRates
|
||||
);
|
||||
|
||||
/**
|
||||
* @route GET /api/v1/admin/config/user-commission-rate/:userId
|
||||
* @desc Get user's commission rate
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.get(
|
||||
'/config/user-commission-rate/:userId',
|
||||
authenticateAdmin,
|
||||
requireRole(['super_admin', 'admin']),
|
||||
adminCommissionController.getUserCommissionRate
|
||||
);
|
||||
|
||||
/**
|
||||
* @route PUT /api/v1/admin/config/user-commission-rate/:userId
|
||||
* @desc Set user's commission rate
|
||||
* @access Private (Super Admin only)
|
||||
*/
|
||||
router.put(
|
||||
'/config/user-commission-rate/:userId',
|
||||
authenticateAdmin,
|
||||
requireRole(['super_admin']),
|
||||
logAdminOperation,
|
||||
adminCommissionController.setUserCommissionRate
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
|||
|
|
@ -37,9 +37,10 @@ const getUserList = async (options = {}) => {
|
|||
// Build where clause
|
||||
const where = {};
|
||||
|
||||
// Search by nickname, real name, phone, or wechat ID
|
||||
// Search by uid, nickname, real name, phone, or wechat ID
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ uid: { [Op.like]: `%${search}%` } },
|
||||
{ nickname: { [Op.like]: `%${search}%` } },
|
||||
{ realName: { [Op.like]: `%${search}%` } },
|
||||
{ phone: { [Op.like]: `%${search}%` } },
|
||||
|
|
@ -370,6 +371,7 @@ const exportUsersToCSV = async (options = {}) => {
|
|||
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ uid: { [Op.like]: `%${search}%` } },
|
||||
{ nickname: { [Op.like]: `%${search}%` } },
|
||||
{ realName: { [Op.like]: `%${search}%` } },
|
||||
{ phone: { [Op.like]: `%${search}%` } },
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ const getWithdrawalList = async (options = {}) => {
|
|||
reviewedAt: withdrawal.reviewedAt,
|
||||
rejectionReason: withdrawal.rejectionReason,
|
||||
completedAt: withdrawal.completedAt,
|
||||
paymentProof: withdrawal.paymentProof,
|
||||
createdAt: withdrawal.createdAt,
|
||||
updatedAt: withdrawal.updatedAt,
|
||||
user: withdrawal.user ? {
|
||||
|
|
@ -113,9 +114,10 @@ const getWithdrawalList = async (options = {}) => {
|
|||
* @param {String} withdrawalId - Withdrawal ID
|
||||
* @param {String} adminId - Admin ID performing the action
|
||||
* @param {String} notes - Optional notes
|
||||
* @param {String} paymentProof - Payment proof image URL (required)
|
||||
* @returns {Promise<Object>} Updated withdrawal
|
||||
*/
|
||||
const approveWithdrawal = async (withdrawalId, adminId, notes = '') => {
|
||||
const approveWithdrawal = async (withdrawalId, adminId, notes = '', paymentProof = '') => {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
|
|
@ -147,6 +149,7 @@ const approveWithdrawal = async (withdrawalId, adminId, notes = '') => {
|
|||
reviewedBy: adminId,
|
||||
reviewedAt: new Date(),
|
||||
completedAt: new Date(),
|
||||
paymentProof: paymentProof || null,
|
||||
}, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
|
|
@ -169,6 +172,7 @@ const approveWithdrawal = async (withdrawalId, adminId, notes = '') => {
|
|||
reviewedBy: withdrawal.reviewedBy,
|
||||
reviewedAt: withdrawal.reviewedAt,
|
||||
completedAt: withdrawal.completedAt,
|
||||
paymentProof: withdrawal.paymentProof,
|
||||
userBalance: currentBalance,
|
||||
};
|
||||
} catch (error) {
|
||||
|
|
@ -290,6 +294,7 @@ const getWithdrawalDetails = async (withdrawalId) => {
|
|||
reviewedAt: withdrawal.reviewedAt,
|
||||
rejectionReason: withdrawal.rejectionReason,
|
||||
completedAt: withdrawal.completedAt,
|
||||
paymentProof: withdrawal.paymentProof,
|
||||
createdAt: withdrawal.createdAt,
|
||||
updatedAt: withdrawal.updatedAt,
|
||||
user: withdrawal.user ? {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const Config = require('../models/Config');
|
||||
const User = require('../models/User');
|
||||
|
||||
/**
|
||||
* Commission Config Service
|
||||
|
|
@ -30,6 +31,101 @@ const getCommissionRate = async () => {
|
|||
return rate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get commission rate for a specific user
|
||||
* Returns user's custom rate if set, otherwise returns system default rate
|
||||
* @param {string} userId - User ID
|
||||
* @returns {number} Commission rate (e.g., 0.02 for 2%)
|
||||
*/
|
||||
const getUserCommissionRate = async (userId) => {
|
||||
if (!userId) {
|
||||
return getCommissionRate();
|
||||
}
|
||||
|
||||
const user = await User.findByPk(userId, {
|
||||
attributes: ['commissionRate'],
|
||||
});
|
||||
|
||||
// If user has custom commission rate, use it
|
||||
if (user && user.commissionRate !== null && user.commissionRate !== undefined) {
|
||||
const rate = parseFloat(user.commissionRate);
|
||||
if (!isNaN(rate) && rate >= 0 && rate <= 1) {
|
||||
return rate;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise use system default rate
|
||||
return getCommissionRate();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set user's custom commission rate
|
||||
* @param {string} userId - User ID
|
||||
* @param {number|null} rate - Commission rate (null to clear custom rate)
|
||||
* @returns {Object} Updated user info
|
||||
*/
|
||||
const setUserCommissionRate = async (userId, rate) => {
|
||||
const user = await User.findByPk(userId);
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
// If rate is null, clear custom rate
|
||||
if (rate === null || rate === '') {
|
||||
user.commissionRate = null;
|
||||
await user.save();
|
||||
return {
|
||||
userId: user.id,
|
||||
uid: user.uid,
|
||||
nickname: user.nickname,
|
||||
commissionRate: null,
|
||||
useSystemDefault: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate rate
|
||||
const rateValue = parseFloat(rate);
|
||||
if (isNaN(rateValue) || rateValue < 0 || rateValue > 1) {
|
||||
throw new Error('Invalid commission rate. Must be between 0 and 1.');
|
||||
}
|
||||
|
||||
user.commissionRate = rateValue;
|
||||
await user.save();
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
uid: user.uid,
|
||||
nickname: user.nickname,
|
||||
commissionRate: rateValue,
|
||||
percentage: `${(rateValue * 100).toFixed(2)}%`,
|
||||
useSystemDefault: false,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get users with custom commission rates
|
||||
* @returns {Array} List of users with custom rates
|
||||
*/
|
||||
const getUsersWithCustomRates = async () => {
|
||||
const users = await User.findAll({
|
||||
where: {
|
||||
commissionRate: {
|
||||
[require('sequelize').Op.ne]: null,
|
||||
},
|
||||
},
|
||||
attributes: ['id', 'uid', 'nickname', 'commissionRate', 'createdAt'],
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
|
||||
return users.map(user => ({
|
||||
userId: user.id,
|
||||
uid: user.uid,
|
||||
nickname: user.nickname,
|
||||
commissionRate: parseFloat(user.commissionRate),
|
||||
percentage: `${(parseFloat(user.commissionRate) * 100).toFixed(2)}%`,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Set commission rate
|
||||
* @param {number} rate - Commission rate (e.g., 0.02 for 2%)
|
||||
|
|
@ -89,7 +185,10 @@ const getCommissionRateConfig = async () => {
|
|||
|
||||
module.exports = {
|
||||
getCommissionRate,
|
||||
getUserCommissionRate,
|
||||
setCommissionRate,
|
||||
setUserCommissionRate,
|
||||
getUsersWithCustomRates,
|
||||
getCommissionRateConfig,
|
||||
DEFAULT_COMMISSION_RATE,
|
||||
COMMISSION_RATE_KEY,
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ const calculateCommission = async (paymentOrderId) => {
|
|||
return null; // User was not invited, no commission
|
||||
}
|
||||
|
||||
// Get current commission rate
|
||||
const commissionRate = await commissionConfigService.getCommissionRate();
|
||||
// Get commission rate for the inviter (user-specific or system default)
|
||||
const commissionRate = await commissionConfigService.getUserCommissionRate(inviteeUser.invitedBy);
|
||||
|
||||
// Collect all currency amounts to process
|
||||
const currencyAmounts = [];
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const generateOrderNo = () => {
|
|||
* @returns {Object} Created payment order with commission info
|
||||
*/
|
||||
const createPaymentOrder = async (data, adminId) => {
|
||||
const { userId, appointmentId, amount, amountPeso, amountRmb, serviceContent, paymentTime, notes } = data;
|
||||
const { userId, appointmentId, amount, amountPeso, amountRmb, serviceContent, paymentTime, notes, paymentProof } = data;
|
||||
|
||||
// Validate required fields
|
||||
if (!userId) {
|
||||
|
|
@ -49,6 +49,9 @@ const createPaymentOrder = async (data, adminId) => {
|
|||
if (!paymentTime) {
|
||||
throw new Error('请选择支付时间');
|
||||
}
|
||||
if (!paymentProof) {
|
||||
throw new Error('请上传支付凭证图片');
|
||||
}
|
||||
|
||||
// Verify user exists - support both UUID and UID (6-digit number)
|
||||
let user;
|
||||
|
|
@ -112,6 +115,7 @@ const createPaymentOrder = async (data, adminId) => {
|
|||
serviceContent,
|
||||
paymentTime: new Date(paymentTime),
|
||||
notes: notes || null,
|
||||
paymentProof: paymentProof || null,
|
||||
createdBy: adminId,
|
||||
status: 'active',
|
||||
});
|
||||
|
|
@ -150,6 +154,7 @@ const createPaymentOrder = async (data, adminId) => {
|
|||
amountRmb: paymentOrder.amountRmb ? parseFloat(paymentOrder.amountRmb).toFixed(2) : null,
|
||||
serviceContent: paymentOrder.serviceContent,
|
||||
paymentTime: paymentOrder.paymentTime,
|
||||
paymentProof: paymentOrder.paymentProof,
|
||||
notes: paymentOrder.notes,
|
||||
status: paymentOrder.status,
|
||||
createdAt: paymentOrder.createdAt,
|
||||
|
|
@ -275,6 +280,7 @@ const getPaymentOrders = async (options = {}) => {
|
|||
amountRmb: order.amountRmb ? parseFloat(order.amountRmb).toFixed(2) : null,
|
||||
serviceContent: order.serviceContent,
|
||||
paymentTime: order.paymentTime,
|
||||
paymentProof: order.paymentProof,
|
||||
notes: order.notes,
|
||||
status: order.status,
|
||||
creator: order.creator,
|
||||
|
|
@ -364,6 +370,7 @@ const getPaymentOrderById = async (orderId) => {
|
|||
amountRmb: order.amountRmb ? parseFloat(order.amountRmb).toFixed(2) : null,
|
||||
serviceContent: order.serviceContent,
|
||||
paymentTime: order.paymentTime,
|
||||
paymentProof: order.paymentProof,
|
||||
notes: order.notes,
|
||||
status: order.status,
|
||||
cancelReason: order.cancelReason,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user