管理后台

This commit is contained in:
18631081161 2026-03-04 00:17:21 +08:00
parent e8439c82c1
commit 461bc64a6f
8 changed files with 234 additions and 21 deletions

View File

@ -622,6 +622,26 @@
至少填写一种货币金额两个都填表示同时支付了两种货币
</el-alert>
</el-form-item>
<el-form-item label="比索成本">
<el-input-number
v-model="paymentForm.costPeso"
:min="0"
:precision="2"
:step="100"
style="width: 180px"
/>
<span style="margin-left: 10px; color: #909399;"> 比索可选</span>
</el-form-item>
<el-form-item label="人民币成本">
<el-input-number
v-model="paymentForm.costRmb"
:min="0"
:precision="2"
:step="10"
style="width: 180px"
/>
<span style="margin-left: 10px; color: #909399;">¥ 人民币可选</span>
</el-form-item>
<el-form-item label="支付时间" prop="paymentTime">
<el-date-picker
v-model="paymentForm.paymentTime"
@ -731,6 +751,8 @@ const paymentForm = reactive({
serviceContent: '',
amountPeso: null,
amountRmb: null,
costPeso: null,
costRmb: null,
paymentTime: '',
paymentProof: '',
notes: ''
@ -979,6 +1001,8 @@ function handleCreatePaymentOrder(row) {
paymentForm.serviceContent = row.service?.titleZh || row.hotService?.name_zh || row.serviceType || ''
paymentForm.amountPeso = null
paymentForm.amountRmb = null
paymentForm.costPeso = null
paymentForm.costRmb = null
//
const now = new Date()
const year = now.getFullYear()
@ -1021,6 +1045,8 @@ async function confirmCreatePaymentOrder() {
appointmentId: paymentForm.appointmentNo,
amountPeso: hasPeso ? paymentForm.amountPeso : undefined,
amountRmb: hasRmb ? paymentForm.amountRmb : undefined,
costPeso: paymentForm.costPeso > 0 ? paymentForm.costPeso : undefined,
costRmb: paymentForm.costRmb > 0 ? paymentForm.costRmb : undefined,
serviceContent: paymentForm.serviceContent,
paymentTime: paymentForm.paymentTime,
paymentProof: paymentForm.paymentProof,

View File

@ -214,6 +214,16 @@
<span class="payment-amount">{{ row.currency === 'PHP' ? '₱' : '¥' }}{{ row.paymentAmount }}</span>
</template>
</el-table-column>
<el-table-column prop="costAmount" label="成本金额" width="120">
<template #default="{ row }">
<span class="cost-amount">{{ row.currency === 'PHP' ? '₱' : '¥' }}{{ row.costAmount }}</span>
</template>
</el-table-column>
<el-table-column prop="profitAmount" label="营利金额" width="120">
<template #default="{ row }">
<span class="profit-amount">{{ row.currency === 'PHP' ? '₱' : '¥' }}{{ row.profitAmount }}</span>
</template>
</el-table-column>
<el-table-column prop="commissionRate" label="佣金比例" width="100" />
<el-table-column prop="commissionAmount" label="佣金金额" width="120">
<template #default="{ row }">
@ -685,6 +695,16 @@ onMounted(() => {
font-weight: 500;
}
.cost-amount {
color: #f56c6c;
font-weight: 500;
}
.profit-amount {
color: #67c23a;
font-weight: 600;
}
.commission-amount {
color: #67c23a;
font-weight: 600;

View File

@ -49,6 +49,22 @@
<span class="statistics-label">比索总额</span>
<span class="statistics-value peso">{{ statistics.totalPeso }}</span>
</div>
<div class="statistics-item">
<span class="statistics-label">人民币成本</span>
<span class="statistics-value cost">¥{{ statistics.totalCostRmb }}</span>
</div>
<div class="statistics-item">
<span class="statistics-label">比索成本</span>
<span class="statistics-value cost">{{ statistics.totalCostPeso }}</span>
</div>
<div class="statistics-item">
<span class="statistics-label">人民币营利</span>
<span class="statistics-value profit">¥{{ statistics.profitRmb }}</span>
</div>
<div class="statistics-item">
<span class="statistics-label">比索营利</span>
<span class="statistics-value profit">{{ statistics.profitPeso }}</span>
</div>
</div>
</el-card>
@ -149,6 +165,14 @@
<el-form-item label="服务内容" prop="serviceContent">
<el-input v-model="createForm.serviceContent" type="textarea" :rows="3" placeholder="输入服务内容" />
</el-form-item>
<el-form-item label="比索成本">
<el-input-number v-model="createForm.costPeso" :min="0" :precision="2" placeholder="比索成本" />
<span class="currency-hint"> 比索可选</span>
</el-form-item>
<el-form-item label="人民币成本">
<el-input-number v-model="createForm.costRmb" :min="0" :precision="2" placeholder="人民币成本" />
<span class="currency-hint">¥ 人民币可选</span>
</el-form-item>
<el-form-item label="支付时间" prop="paymentTime">
<el-date-picker
v-model="createForm.paymentTime"
@ -203,6 +227,13 @@
<span v-if="!currentOrder.amountPeso && !currentOrder.amountRmb && currentOrder.amount">¥{{ currentOrder.amount }}</span>
</div>
</el-descriptions-item>
<el-descriptions-item label="成本金额">
<div class="amount-detail">
<span v-if="currentOrder.costPeso" class="peso">{{ currentOrder.costPeso }} (比索)</span>
<span v-if="currentOrder.costRmb" class="rmb">¥{{ currentOrder.costRmb }} (人民币)</span>
<span v-if="!currentOrder.costPeso && !currentOrder.costRmb">-</span>
</div>
</el-descriptions-item>
<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>
@ -295,6 +326,10 @@ const filters = reactive({
const statistics = reactive({
totalRmb: '0.00',
totalPeso: '0.00',
totalCostRmb: '0.00',
totalCostPeso: '0.00',
profitRmb: '0.00',
profitPeso: '0.00',
})
const pagination = reactive({
@ -307,6 +342,8 @@ const createForm = reactive({
userId: '',
amountPeso: null,
amountRmb: null,
costPeso: null,
costRmb: null,
serviceContent: '',
paymentTime: '',
paymentProof: '',
@ -346,9 +383,17 @@ async function fetchOrders() {
if (response.data.data.statistics) {
statistics.totalRmb = response.data.data.statistics.totalRmb || '0.00'
statistics.totalPeso = response.data.data.statistics.totalPeso || '0.00'
statistics.totalCostRmb = response.data.data.statistics.totalCostRmb || '0.00'
statistics.totalCostPeso = response.data.data.statistics.totalCostPeso || '0.00'
statistics.profitRmb = response.data.data.statistics.profitRmb || '0.00'
statistics.profitPeso = response.data.data.statistics.profitPeso || '0.00'
} else {
statistics.totalRmb = '0.00'
statistics.totalPeso = '0.00'
statistics.totalCostRmb = '0.00'
statistics.totalCostPeso = '0.00'
statistics.profitRmb = '0.00'
statistics.profitPeso = '0.00'
}
}
} catch (error) {
@ -399,6 +444,8 @@ function showCreateDialog() {
userId: '',
amountPeso: null,
amountRmb: null,
costPeso: null,
costRmb: null,
serviceContent: '',
paymentTime: currentTime,
paymentProof: '',
@ -471,6 +518,8 @@ async function handleCreate() {
userId: createForm.userId,
amountPeso: hasPeso ? createForm.amountPeso : undefined,
amountRmb: hasRmb ? createForm.amountRmb : undefined,
costPeso: createForm.costPeso > 0 ? createForm.costPeso : undefined,
costRmb: createForm.costRmb > 0 ? createForm.costRmb : undefined,
serviceContent: createForm.serviceContent,
paymentTime: createForm.paymentTime,
paymentProof: createForm.paymentProof,
@ -577,6 +626,7 @@ onMounted(() => {
.statistics-wrapper {
display: flex;
flex-wrap: wrap;
gap: 40px;
}
@ -602,6 +652,14 @@ onMounted(() => {
&.peso {
color: #409eff;
}
&.cost {
color: #f56c6c;
}
&.profit {
color: #67c23a;
}
}
}

View File

@ -0,0 +1,45 @@
/**
* Migration: Add cost_peso and cost_rmb fields to payment_orders table
*/
const { sequelize } = require('./src/config/database');
async function migrate() {
try {
const queryInterface = sequelize.getQueryInterface();
// Check if columns already exist
const tableDesc = await queryInterface.describeTable('payment_orders');
if (!tableDesc.cost_peso) {
await queryInterface.addColumn('payment_orders', 'cost_peso', {
type: require('sequelize').DataTypes.DECIMAL(10, 2),
allowNull: true,
defaultValue: null,
comment: '成本金额(比索)',
});
console.log('Added cost_peso column');
} else {
console.log('cost_peso column already exists');
}
if (!tableDesc.cost_rmb) {
await queryInterface.addColumn('payment_orders', 'cost_rmb', {
type: require('sequelize').DataTypes.DECIMAL(10, 2),
allowNull: true,
defaultValue: null,
comment: '成本金额(人民币)',
});
console.log('Added cost_rmb column');
} else {
console.log('cost_rmb column already exists');
}
console.log('Migration completed successfully');
process.exit(0);
} catch (error) {
console.error('Migration failed:', error);
process.exit(1);
}
}
migrate();

View File

@ -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, paymentProof } = req.body;
const { userId, appointmentId, amount, amountPeso, amountRmb, costPeso, costRmb, serviceContent, paymentTime, notes, paymentProof } = req.body;
const result = await paymentOrderService.createPaymentOrder({
userId: userId ? userId.trim() : userId, // Trim whitespace
@ -20,6 +20,8 @@ const createPaymentOrder = async (req, res) => {
amount,
amountPeso,
amountRmb,
costPeso,
costRmb,
serviceContent,
paymentTime,
notes,

View File

@ -97,6 +97,20 @@ const PaymentOrder = sequelize.define('PaymentOrder', {
field: 'cancel_reason',
comment: '取消原因',
},
costPeso: {
type: DataTypes.DECIMAL(10, 2),
allowNull: true,
defaultValue: null,
field: 'cost_peso',
comment: '成本金额(比索)',
},
costRmb: {
type: DataTypes.DECIMAL(10, 2),
allowNull: true,
defaultValue: null,
field: 'cost_rmb',
comment: '成本金额(人民币)',
},
paymentProof: {
type: DataTypes.STRING(500),
allowNull: true,

View File

@ -43,14 +43,18 @@ const calculateCommission = async (paymentOrderId) => {
// Check RMB amount (including legacy 'amount' field)
const rmbAmount = parseFloat(paymentOrder.amountRmb || 0) || parseFloat(paymentOrder.amount || 0);
const rmbCost = parseFloat(paymentOrder.costRmb || 0);
const rmbProfit = rmbAmount - rmbCost;
if (rmbAmount > 0) {
currencyAmounts.push({ currency: 'RMB', paymentAmount: rmbAmount });
currencyAmounts.push({ currency: 'RMB', paymentAmount: rmbAmount, profitAmount: rmbProfit > 0 ? rmbProfit : 0 });
}
// Check Peso amount
const pesoAmount = parseFloat(paymentOrder.amountPeso || 0);
const pesoCost = parseFloat(paymentOrder.costPeso || 0);
const pesoProfit = pesoAmount - pesoCost;
if (pesoAmount > 0) {
currencyAmounts.push({ currency: 'PHP', paymentAmount: pesoAmount });
currencyAmounts.push({ currency: 'PHP', paymentAmount: pesoAmount, profitAmount: pesoProfit > 0 ? pesoProfit : 0 });
}
if (currencyAmounts.length === 0) {
@ -67,8 +71,9 @@ const calculateCommission = async (paymentOrderId) => {
const inviter = await User.findByPk(inviteeUser.invitedBy, { transaction });
// Create commission record for each currency
for (const { currency, paymentAmount } of currencyAmounts) {
const commissionAmount = paymentAmount * commissionRate;
for (const { currency, paymentAmount, profitAmount } of currencyAmounts) {
// Commission = profit * rate (profit = payment - cost)
const commissionAmount = profitAmount * commissionRate;
// Create commission record
const commission = await Commission.create({
@ -374,7 +379,7 @@ const getAllCommissions = async (options = {}) => {
{
model: PaymentOrder,
as: 'paymentOrder',
attributes: ['id', 'orderNo', 'amount'],
attributes: ['id', 'orderNo', 'amount', 'amountPeso', 'amountRmb', 'costPeso', 'costRmb'],
},
],
order: [['createdAt', 'DESC']],
@ -382,18 +387,40 @@ const getAllCommissions = async (options = {}) => {
offset: parseInt(offset),
});
const records = rows.map(commission => ({
id: commission.id,
inviter: commission.inviter,
invitee: commission.invitee,
paymentOrder: commission.paymentOrder,
paymentAmount: parseFloat(commission.paymentAmount).toFixed(2),
commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`,
commissionAmount: parseFloat(commission.commissionAmount).toFixed(2),
currency: commission.currency || 'RMB',
status: commission.status,
createdAt: commission.createdAt,
}));
const records = rows.map(commission => {
const po = commission.paymentOrder;
const currency = commission.currency || 'RMB';
// Calculate cost and profit for this commission's currency
let costAmount = 0;
let profitAmount = 0;
if (po) {
if (currency === 'PHP') {
costAmount = parseFloat(po.costPeso || 0);
const payAmt = parseFloat(po.amountPeso || 0);
profitAmount = payAmt - costAmount;
} else {
costAmount = parseFloat(po.costRmb || 0);
const payAmt = parseFloat(po.amountRmb || 0) || parseFloat(po.amount || 0);
profitAmount = payAmt - costAmount;
}
}
return {
id: commission.id,
inviter: commission.inviter,
invitee: commission.invitee,
paymentOrder: po ? { id: po.id, orderNo: po.orderNo } : null,
paymentAmount: parseFloat(commission.paymentAmount).toFixed(2),
costAmount: costAmount.toFixed(2),
profitAmount: profitAmount.toFixed(2),
commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`,
commissionAmount: parseFloat(commission.commissionAmount).toFixed(2),
currency: currency,
status: commission.status,
createdAt: commission.createdAt,
};
});
return {
records,

View File

@ -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, paymentProof } = data;
const { userId, appointmentId, amount, amountPeso, amountRmb, costPeso, costRmb, serviceContent, paymentTime, notes, paymentProof } = data;
// Validate required fields
if (!userId) {
@ -104,6 +104,10 @@ const createPaymentOrder = async (data, adminId) => {
// Calculate total amount for commission (check any currency)
const totalAmountForCommission = rmbAmount > 0 ? rmbAmount : (pesoAmount > 0 ? pesoAmount : legacyAmount);
// Parse cost amounts
const costPesoAmount = costPeso !== null && costPeso !== undefined ? parseFloat(costPeso) : 0;
const costRmbAmount = costRmb !== null && costRmb !== undefined ? parseFloat(costRmb) : 0;
// Create payment order
const paymentOrder = await PaymentOrder.create({
orderNo,
@ -112,6 +116,8 @@ const createPaymentOrder = async (data, adminId) => {
amount: legacyAmount > 0 ? legacyAmount : null, // Legacy field
amountPeso: pesoAmount > 0 ? pesoAmount : null,
amountRmb: rmbAmount > 0 ? rmbAmount : null,
costPeso: costPesoAmount > 0 ? costPesoAmount : null,
costRmb: costRmbAmount > 0 ? costRmbAmount : null,
serviceContent,
paymentTime: new Date(paymentTime),
notes: notes || null,
@ -152,6 +158,8 @@ const createPaymentOrder = async (data, adminId) => {
amount: paymentOrder.amount ? parseFloat(paymentOrder.amount).toFixed(2) : null,
amountPeso: paymentOrder.amountPeso ? parseFloat(paymentOrder.amountPeso).toFixed(2) : null,
amountRmb: paymentOrder.amountRmb ? parseFloat(paymentOrder.amountRmb).toFixed(2) : null,
costPeso: paymentOrder.costPeso ? parseFloat(paymentOrder.costPeso).toFixed(2) : null,
costRmb: paymentOrder.costRmb ? parseFloat(paymentOrder.costRmb).toFixed(2) : null,
serviceContent: paymentOrder.serviceContent,
paymentTime: paymentOrder.paymentTime,
paymentProof: paymentOrder.paymentProof,
@ -233,6 +241,8 @@ const getPaymentOrders = async (options = {}) => {
attributes: [
[sequelize.fn('COALESCE', sequelize.fn('SUM', sequelize.col('amount_rmb')), 0), 'totalRmb'],
[sequelize.fn('COALESCE', sequelize.fn('SUM', sequelize.col('amount_peso')), 0), 'totalPeso'],
[sequelize.fn('COALESCE', sequelize.fn('SUM', sequelize.col('cost_rmb')), 0), 'totalCostRmb'],
[sequelize.fn('COALESCE', sequelize.fn('SUM', sequelize.col('cost_peso')), 0), 'totalCostPeso'],
],
raw: true,
}),
@ -241,9 +251,18 @@ const getPaymentOrders = async (options = {}) => {
const { count, rows } = ordersResult;
// Format statistics with two decimal places
const totalRmb = parseFloat(statisticsResult?.totalRmb || 0);
const totalPeso = parseFloat(statisticsResult?.totalPeso || 0);
const totalCostRmb = parseFloat(statisticsResult?.totalCostRmb || 0);
const totalCostPeso = parseFloat(statisticsResult?.totalCostPeso || 0);
const statistics = {
totalRmb: parseFloat(statisticsResult?.totalRmb || 0).toFixed(2),
totalPeso: parseFloat(statisticsResult?.totalPeso || 0).toFixed(2),
totalRmb: totalRmb.toFixed(2),
totalPeso: totalPeso.toFixed(2),
totalCostRmb: totalCostRmb.toFixed(2),
totalCostPeso: totalCostPeso.toFixed(2),
profitRmb: (totalRmb - totalCostRmb).toFixed(2),
profitPeso: (totalPeso - totalCostPeso).toFixed(2),
};
// Get all commissions for each order (dual-currency support)
@ -368,6 +387,8 @@ const getPaymentOrderById = async (orderId) => {
amount: order.amount ? parseFloat(order.amount).toFixed(2) : null,
amountPeso: order.amountPeso ? parseFloat(order.amountPeso).toFixed(2) : null,
amountRmb: order.amountRmb ? parseFloat(order.amountRmb).toFixed(2) : null,
costPeso: order.costPeso ? parseFloat(order.costPeso).toFixed(2) : null,
costRmb: order.costRmb ? parseFloat(order.costRmb).toFixed(2) : null,
serviceContent: order.serviceContent,
paymentTime: order.paymentTime,
paymentProof: order.paymentProof,