@@ -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;
diff --git a/admin/src/views/payment-orders/index.vue b/admin/src/views/payment-orders/index.vue
index b86f9a0..486d85d 100644
--- a/admin/src/views/payment-orders/index.vue
+++ b/admin/src/views/payment-orders/index.vue
@@ -49,6 +49,22 @@
比索总额
₱{{ statistics.totalPeso }}
+
+ 人民币成本
+ ¥{{ statistics.totalCostRmb }}
+
+
+ 比索成本
+ ₱{{ statistics.totalCostPeso }}
+
+
+ 人民币营利
+ ¥{{ statistics.profitRmb }}
+
+
+ 比索营利
+ ₱{{ statistics.profitPeso }}
+
@@ -149,6 +165,14 @@
+
+
+ ₱ 比索(可选)
+
+
+
+ ¥ 人民币(可选)
+
¥{{ currentOrder.amount }}
+
+
+ ₱{{ currentOrder.costPeso }} (比索)
+ ¥{{ currentOrder.costRmb }} (人民币)
+ -
+
+
{{ currentOrder.serviceContent }}
{{ formatDate(currentOrder.paymentTime) }}
{{ formatDate(currentOrder.createdAt) }}
@@ -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;
+ }
}
}
diff --git a/backend/add-cost-fields.js b/backend/add-cost-fields.js
new file mode 100644
index 0000000..d033821
--- /dev/null
+++ b/backend/add-cost-fields.js
@@ -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();
diff --git a/backend/src/controllers/adminPaymentOrderController.js b/backend/src/controllers/adminPaymentOrderController.js
index c06b967..05b82f0 100644
--- a/backend/src/controllers/adminPaymentOrderController.js
+++ b/backend/src/controllers/adminPaymentOrderController.js
@@ -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,
diff --git a/backend/src/models/PaymentOrder.js b/backend/src/models/PaymentOrder.js
index 3f91026..c1af93b 100644
--- a/backend/src/models/PaymentOrder.js
+++ b/backend/src/models/PaymentOrder.js
@@ -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,
diff --git a/backend/src/services/commissionService.js b/backend/src/services/commissionService.js
index 6c81daa..83890ec 100644
--- a/backend/src/services/commissionService.js
+++ b/backend/src/services/commissionService.js
@@ -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,
diff --git a/backend/src/services/paymentOrderService.js b/backend/src/services/paymentOrderService.js
index 2932298..d697fc1 100644
--- a/backend/src/services/paymentOrderService.js
+++ b/backend/src/services/paymentOrderService.js
@@ -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,