Files
jurong_circle_black/services/matchingService.js

1408 lines
51 KiB
JavaScript
Raw Normal View History

2025-08-26 10:06:23 +08:00
const { getDB } = require('../database');
const timeoutService = require('./timeoutService');
const dayjs = require('dayjs');
/**
* 获取本地时区的日期字符串YYYY-MM-DD格式
* 确保在晚上12点正确切换到第二天
* @param {Date} date - 可选的日期对象默认为当前时间
* @returns {string} 格式化的日期字符串
*/
function getLocalDateString(date = new Date()) {
return dayjs(date).format('YYYY-MM-DD');
}
class MatchingService {
// 创建匹配订单(支持两种模式)
async createMatchingOrder(userId, matchingType = 'small', customAmount = null) {
const db = getDB();
try {
// 检查用户是否被拉黑
const isBlacklisted = await timeoutService.isUserBlacklisted(userId);
if (isBlacklisted) {
throw new Error('您已被拉黑,无法参与匹配。如有疑问请联系管理员。');
}
// 检查用户审核状态、必要信息和余额
const [userResult] = await db.execute(
'SELECT audit_status, balance, wechat_qr, alipay_qr, unionpay_qr, bank_card, business_license, id_card_front, id_card_back FROM users WHERE id = ?',
[userId]
);
if (userResult.length === 0) {
throw new Error('用户不存在');
}
const user = userResult[0];
// 检查用户余额:只有负余额用户才能发起匹配
// if (user.balance > 0) {
// throw new Error('只有余额为负数的用户才能发起匹配,这是为了防止公司资金损失的重要规则');
// }
// 检查用户审核状态
if (user.audit_status !== 'approved') {
if (user.audit_status === 'pending') {
throw new Error('您的账户正在审核中,审核通过后才能参与匹配');
} else if (user.audit_status === 'rejected') {
throw new Error('您的账户审核未通过,请联系管理员');
}
}
// 检查必要的收款信息是否已上传
const missingItems = [];
if (!user.wechat_qr && !user.alipay_qr && !user.unionpay_qr) {
missingItems.push('收款码(微信/支付宝/云闪付至少一种)');
}
if (!user.bank_card) {
missingItems.push('银行卡号');
}
if (!user.business_license) {
missingItems.push('营业执照');
}
if (!user.id_card_front || !user.id_card_back) {
missingItems.push('身份证正反面');
}
if (missingItems.length > 0) {
throw new Error(`请先上传以下信息:${missingItems.join('、')}`);
}
await db.query('START TRANSACTION');
let totalAmount, maxCycles;
if (matchingType === 'small') {
// 小额匹配固定5000元金额
totalAmount = 5000;
maxCycles = 1;
} else if (matchingType === 'large') {
// 大额匹配用户自定义金额最高5万
if (!customAmount || customAmount < 5000 || customAmount > 50000) {
throw new Error('大额匹配金额必须在5000-50000之间');
}
totalAmount = customAmount;
maxCycles = 1;
} else {
throw new Error('不支持的匹配类型');
}
// 创建匹配订单
const [result] = await db.execute(
'INSERT INTO matching_orders (initiator_id, amount, status, max_cycles, matching_type) VALUES (?, ?, "matching", ?, ?)',
[userId, totalAmount, maxCycles, matchingType]
);
const orderId = result.insertId;
// 记录用户参与
await db.execute(
'INSERT INTO matching_records (matching_order_id, user_id, action, amount) VALUES (?, ?, "join", ?)',
[orderId, userId, totalAmount]
);
await db.query('COMMIT');
// 立即生成智能分配
const allocations = await this.generateSmartAllocationsWithDB(orderId, userId);
// 检查并触发系统账户反向匹配
// await this.checkAndTriggerSystemMatching();
return {
orderId,
matchingType,
totalAmount,
allocations: allocations || [],
allocationCount: allocations ? allocations.length : 0
};
} catch (error) {
await db.query('ROLLBACK');
console.error('创建匹配订单失败:', error);
throw error;
}
}
/**
* 检查资金平衡并触发系统账户反向匹配
* 当收款需求大于打款资金时系统账户主动发起匹配
*/
async checkAndTriggerSystemMatching() {
const db = getDB();
try {
// 计算当前待收款总额(负余额用户的资金缺口)
const [negativeBalanceResult] = await db.execute(`
SELECT SUM(ABS(balance)) as total_deficit
FROM users
WHERE is_system_account = FALSE AND balance < 0
`);
const totalDeficit = negativeBalanceResult[0].total_deficit || 0;
// 计算当前待打款总额pending状态的分配
const [pendingPaymentsResult] = await db.execute(`
SELECT SUM(oa.amount) as total_pending
FROM transfers oa
JOIN users u ON oa.from_user_id = u.id
WHERE oa.status = 'pending' AND u.is_system_account = FALSE
`);
const totalPendingPayments = pendingPaymentsResult[0].total_pending || 0;
console.log(`资金平衡检查: 总资金缺口=${totalDeficit}, 待打款总额=${totalPendingPayments}`);
// 如果收款需求大于打款资金,触发系统账户反向匹配
if (totalDeficit > totalPendingPayments) {
const shortfall = totalDeficit - totalPendingPayments;
console.log(`检测到资金缺口: ${shortfall}元,触发系统账户反向匹配`);
await this.createSystemReverseMatching(shortfall);
}
} catch (error) {
console.error('检查资金平衡失败:', error);
// 不抛出错误,避免影响主流程
}
}
/**
* 创建系统账户反向匹配
* 系统账户作为付款方向有资金缺口的用户打款
* @param {number} targetAmount - 目标匹配金额
*/
async createSystemReverseMatching(targetAmount) {
const db = getDB();
try {
// 获取可用的系统账户
const [systemAccounts] = await db.execute(`
SELECT id, balance FROM users
WHERE is_system_account = TRUE AND balance > 1000
ORDER BY balance DESC
LIMIT 1
`);
if (systemAccounts.length === 0) {
console.log('没有可用的系统账户进行反向匹配');
return;
}
const systemAccount = systemAccounts[0];
// 确定实际匹配金额不超过系统账户余额的80%
const maxMatchAmount = Math.min(targetAmount, systemAccount.balance * 0.8);
if (maxMatchAmount < 1000) {
console.log('系统账户余额不足,无法进行反向匹配');
return;
}
// 创建系统反向匹配订单
const [result] = await db.execute(
'INSERT INTO matching_orders (initiator_id, amount, status, max_cycles, matching_type, is_system_reverse) VALUES (?, ?, "matching", 1, "system_reverse", TRUE)',
[systemAccount.id, maxMatchAmount]
);
const orderId = result.insertId;
// 生成分配给负余额用户
await this.generateSystemReverseAllocations(orderId, maxMatchAmount, systemAccount.id);
console.log(`系统反向匹配创建成功: 订单ID=${orderId}, 金额=${maxMatchAmount}`);
} catch (error) {
console.error('创建系统反向匹配失败:', error);
}
}
/**
* 为系统反向匹配生成分配
* @param {number} orderId - 匹配订单ID
* @param {number} totalAmount - 总金额
* @param {number} systemUserId - 系统账户ID
*/
async generateSystemReverseAllocations(orderId, totalAmount, systemUserId) {
const db = getDB();
try {
// 获取负余额用户,按缺口大小排序
const [negativeUsers] = await db.execute(`
SELECT id, balance, ABS(balance) as deficit
FROM users
WHERE is_system_account = FALSE AND balance < 0
ORDER BY deficit DESC
LIMIT 10
`);
if (negativeUsers.length === 0) {
console.log('没有负余额用户需要反向匹配');
return;
}
// 按比例分配金额给负余额用户
const totalDeficit = negativeUsers.reduce((sum, user) => sum + user.deficit, 0);
let remainingAmount = totalAmount;
for (let i = 0; i < negativeUsers.length && remainingAmount > 0; i++) {
const user = negativeUsers[i];
let allocationAmount;
if (i === negativeUsers.length - 1) {
// 最后一个用户分配剩余金额
allocationAmount = remainingAmount;
} else {
// 按比例分配
const proportion = user.deficit / totalDeficit;
allocationAmount = Math.min(
Math.floor(totalAmount * proportion),
user.deficit,
remainingAmount
);
}
if (allocationAmount > 0) {
// 创建分配记录(系统账户向用户转账)
await db.execute(
'INSERT INTO transfers (matching_order_id, from_user_id, to_user_id, amount, cycle_number, status) VALUES (?, ?, ?, ?, 1, "pending")',
[orderId, systemUserId, user.id, allocationAmount]
);
remainingAmount -= allocationAmount;
console.log(`系统反向分配: ${allocationAmount}元 从系统账户${systemUserId} 到用户${user.id}`);
}
}
} catch (error) {
console.error('生成系统反向分配失败:', error);
throw error;
}
}
/**
* 生成大额匹配的随机金额分拆15000以上
* @param {number} totalAmount - 总金额
* @returns {Array} 分拆后的金额数组
*/
generateRandomLargeAmounts(totalAmount) {
const amounts = [];
let remaining = totalAmount;
const minAmount = 1000; // 最小单笔金额
const maxAmount = 8000; // 最大单笔金额
while (remaining > maxAmount) {
// 生成随机金额,确保剩余金额至少还能分一笔
const maxThisAmount = Math.min(maxAmount, remaining - minAmount);
const amount = Math.floor(Math.random() * (maxThisAmount - minAmount + 1)) + minAmount;
amounts.push(amount);
remaining -= amount;
}
// 最后一笔是剩余金额
if (remaining > 0) {
amounts.push(remaining);
}
return amounts;
}
/**
* 生成3笔分配兼容旧版本接口
* 确保不会分配给同一个用户
* @param {number} orderId - 订单ID
* @param {Array} amounts - 金额数组
* @param {number} initiatorId - 发起人ID
*/
/**
* 验证匹配金额是否符合业务规则
* @param {number} userId - 用户ID
* @param {number} totalAmount - 总匹配金额
* @param {Array} amounts - 分配金额数组
* @returns {Object} 验证结果和建议金额
*/
async validateMatchingAmount(userId, totalAmount, amounts) {
const db = getDB();
try {
// 获取昨天的日期(本地时区)
const yesterdayStr = dayjs().subtract(1, 'day').format('YYYY-MM-DD');
// 获取前一天所有用户的出款总数(系统总出款)
const [systemOutboundResult] = await db.execute(
`SELECT SUM(oa.amount) as total_outbound
FROM transfers oa
JOIN users u ON oa.from_user_id = u.id
WHERE DATE(oa.outbound_date) = ? AND oa.status = 'confirmed' AND u.is_system_account = FALSE`,
[yesterdayStr]
);
const systemOutbound = systemOutboundResult[0].total_outbound || 0;
// 获取前一天所有用户的具体出款金额(用于检查重复)
const [yesterdayAmountsResult] = await db.execute(
`SELECT DISTINCT oa.amount
FROM transfers oa
JOIN users u ON oa.from_user_id = u.id
WHERE DATE(oa.outbound_date) = ? AND oa.status = 'confirmed' AND u.is_system_account = FALSE`,
[yesterdayStr]
);
const yesterdayAmounts = yesterdayAmountsResult.map(row => row.amount);
// 检查每笔金额是否与前一天的金额不同
const duplicateAmounts = [];
for (const amount of amounts) {
if (yesterdayAmounts.includes(amount)) {
duplicateAmounts.push(amount);
}
}
return {
isValid: duplicateAmounts.length === 0,
systemOutbound,
duplicateAmounts,
suggestedAmount: systemOutbound,
message: duplicateAmounts.length > 0
? `以下金额与前一天重复: ${duplicateAmounts.join(', ')}`
: '匹配金额验证通过'
};
} catch (error) {
console.error('验证匹配金额失败:', error);
return {
isValid: false,
systemOutbound: 0,
duplicateAmounts: [],
suggestedAmount: 0,
message: '验证匹配金额时发生错误'
};
}
}
/**
* 生成智能分配并创建数据库记录
* @param {number} orderId - 订单ID
* @param {number} initiatorId - 发起人ID
* @returns {Promise<Array>} 返回分配结果数组
*/
async generateSmartAllocationsWithDB(orderId, initiatorId) {
const db = getDB();
try {
// 获取订单总金额
const [orderResult] = await db.execute(
'SELECT amount FROM matching_orders WHERE id = ?',
[orderId]
);
if (orderResult.length === 0) {
throw new Error('匹配订单不存在');
}
const totalAmount = orderResult[0].amount;
// 使用智能分配算法生成分配方案
const allocations = await this.generateSmartAllocations(totalAmount, initiatorId);
if (allocations.length === 0) {
throw new Error('无法生成有效的分配方案');
}
// 验证总金额(简化版验证)
const totalAllocated = allocations.reduce((sum, allocation) => sum + allocation.amount, 0);
if (Math.abs(totalAllocated - totalAmount) > 0.01) {
throw new Error(`分配金额不匹配:期望${totalAmount}元,实际分配${totalAllocated}`);
}
console.log(`智能分配验证通过: 用户${initiatorId}, 匹配金额${totalAmount}元, 分配${allocations.length}`);
// 创建分配记录
const createdAllocations = [];
for (let i = 0; i < allocations.length; i++) {
const allocation = allocations[i];
// 设置出款日期为今天可回款时间为明天的00:00:00
const today = dayjs();
const tomorrow = dayjs().add(1, 'day').startOf('day');
const [result] = await db.execute(
'INSERT INTO transfers (matching_order_id, from_user_id, to_user_id, amount, cycle_number, status, outbound_date, can_return_after) VALUES (?, ?, ?, ?, 1, "pending", CURDATE(), ?)',
[orderId, initiatorId, allocation.userId, allocation.amount, tomorrow.format('YYYY-MM-DD HH:mm:ss')]
);
// 添加分配ID到结果中
const createdAllocation = {
...allocation,
allocationId: result.insertId,
status: 'pending',
outboundDate: today.format('YYYY-MM-DD'),
canReturnAfter: tomorrow.toISOString()
};
createdAllocations.push(createdAllocation);
console.log(`创建智能分配: ${allocation.amount}元 从用户${initiatorId} 到用户${allocation.userId}(${allocation.username}) [${allocation.userType}]`);
}
return createdAllocations;
} catch (error) {
console.error('生成智能分配失败:', error);
throw error;
}
}
/**
* 生成传统三笔分配保留原方法用于兼容性
* @param {number} orderId - 订单ID
* @param {Array} amounts - 分配金额数组
* @param {number} initiatorId - 发起人ID
* @returns {Promise<void>}
*/
async generateThreeAllocations(orderId, amounts, initiatorId) {
const db = getDB();
try {
// 获取订单总金额
const [orderResult] = await db.execute(
'SELECT amount FROM matching_orders WHERE id = ?',
[orderId]
);
if (orderResult.length === 0) {
throw new Error('匹配订单不存在');
}
const totalAmount = orderResult[0].amount;
// 验证匹配金额是否符合业务规则
const validation = await this.validateMatchingAmount(initiatorId, totalAmount, amounts);
if (!validation.isValid) {
throw new Error(`匹配金额不符合业务规则:${validation.message}。建议匹配金额:${validation.suggestedAmount}`);
}
// 记录验证信息
console.log(`匹配金额验证通过: 用户${initiatorId}, 匹配金额${totalAmount}元, 前一天系统出款${validation.systemOutbound}`);
const usedTargetUsers = new Set(); // 记录已使用的目标用户
for (let i = 0; i < amounts.length; i++) {
const amount = amounts[i];
// 获取匹配目标,排除已使用的用户
const targetUser = await this.getMatchingTargetExcluding(initiatorId, usedTargetUsers);
if (!targetUser) {
throw new Error(`无法为第${i + 1}笔分配找到匹配目标`);
}
// 记录已使用的目标用户
usedTargetUsers.add(targetUser);
// 创建分配记录默认为第1轮
// 设置出款日期为今天可回款时间为明天的00:00:00
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
// 将Date对象转换为MySQL兼容的字符串格式
const tomorrowStr = tomorrow.toISOString().slice(0, 19).replace('T', ' ');
await db.execute(
'INSERT INTO transfers (matching_order_id, from_user_id, to_user_id, amount, cycle_number, status, outbound_date, can_return_after) VALUES (?, ?, ?, ?, 1, "pending", CURDATE(), ?)',
[orderId, initiatorId, targetUser, amount, tomorrowStr]
);
console.log(`创建分配: ${amount}元 从用户${initiatorId} 到用户${targetUser}`);
}
} catch (error) {
console.error('生成分配失败:', error);
throw error;
}
}
/**
* 获取匹配目标用户
* @param {number} excludeUserId - 要排除的用户ID
* @returns {number} 目标用户ID
*/
/**
* 获取匹配目标用户排除指定用户集合
* @param {number} excludeUserId - 要排除的发起人用户ID
* @param {Set} excludeUserIds - 要排除的用户ID集合
* @returns {number} 目标用户ID
*/
async getMatchingTargetExcluding(excludeUserId, excludeUserIds = new Set()) {
const db = getDB();
try {
// 获取今天和昨天的日期(本地时区)
const today = new Date();
const todayStr = getLocalDateString(today);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = getLocalDateString(yesterday);
// 获取前一天打款的用户ID列表需要排除
const [yesterdayPayersResult] = await db.execute(
`SELECT DISTINCT oa.from_user_id
FROM transfers oa
WHERE DATE(oa.outbound_date) = ? AND oa.status = 'confirmed'`,
[yesterdayStr]
);
const yesterdayPayers = yesterdayPayersResult.map(row => row.from_user_id);
// 获取待确认/待处理/即将生成匹配金额总和超过0的普通用户需要排除
const [pendingUsersResult] = await db.execute(
`SELECT oa.to_user_id, SUM(oa.amount) as pending_amount
FROM transfers oa
JOIN users u ON oa.to_user_id = u.id
WHERE oa.status IN ('pending', 'processing', 'generating')
AND u.is_system_account = FALSE
GROUP BY oa.to_user_id
HAVING pending_amount > 0`
);
const pendingUsers = pendingUsersResult.map(row => row.to_user_id);
// 获取当天有转出订单的普通用户(需要排除)
const [todayPayersResult] = await db.execute(
`SELECT DISTINCT oa.from_user_id
FROM transfers oa
JOIN users u ON oa.from_user_id = u.id
WHERE DATE(oa.created_at) = ?
AND oa.status IN ('confirmed', 'pending', 'processing')
AND u.is_system_account = FALSE`,
[todayStr]
);
const todayPayers = todayPayersResult.map(row => row.from_user_id);
// 构建排除用户的条件(包括发起人、已使用的用户、昨天打款的用户、待处理用户、当天转出用户)
const excludeList = [
excludeUserId,
...Array.from(excludeUserIds),
...yesterdayPayers,
...pendingUsers,
...todayPayers
];
const placeholders = excludeList.map(() => '?').join(',');
// 第一优先级:最早成为负数且通过前面检查的普通用户
// 使用最早的转出记录时间作为成为负数的参考时间
const [earliestNegativeUsers] = await db.execute(
`SELECT u.id, u.balance,
(SELECT MIN(t.created_at) FROM transfers t
WHERE t.from_user_id = u.id AND t.status IN ('confirmed', 'received')) as first_transfer_time
FROM users u
WHERE u.id NOT IN (${placeholders})
AND u.is_system_account = FALSE
AND u.balance < 0
AND (SELECT COUNT(*) FROM transfers t
WHERE t.from_user_id = u.id AND t.status IN ('confirmed', 'received')) > 0
ORDER BY first_transfer_time ASC, u.balance ASC, RAND()
LIMIT 1`,
excludeList
);
if (earliestNegativeUsers.length > 0) {
return earliestNegativeUsers[0].id;
}
// 第二优先级:有可回款分配的普通用户(昨天或更早出款,今天可以回款)
// 但必须是负余额用户,且通过前面的检查
const [returnableUsers] = await db.execute(
`SELECT DISTINCT oa.from_user_id as id, u.balance
FROM transfers oa
JOIN matching_orders mo ON oa.id = mo.id
JOIN users u ON oa.from_user_id = u.id
WHERE oa.from_user_id NOT IN (${placeholders})
AND oa.status = 'confirmed'
AND oa.can_return_after <= NOW()
AND oa.return_date IS NULL
AND mo.status = 'matching'
AND u.balance < 0
AND u.is_system_account = FALSE
ORDER BY oa.can_return_after ASC, u.balance ASC, RAND()
LIMIT 1`,
excludeList
);
if (returnableUsers.length > 0) {
return returnableUsers[0].id;
}
// 第三优先级:其他负余额普通用户(余额为负数说明他们给其他用户转过钱,钱还没收回来)
const [negativeBalanceUsers] = await db.execute(
`SELECT id FROM users
WHERE id NOT IN (${placeholders})
AND is_system_account = FALSE
AND balance < 0
ORDER BY balance ASC, RAND()
LIMIT 1`,
excludeList
);
if (negativeBalanceUsers.length > 0) {
return negativeBalanceUsers[0].id;
}
// 最后优先级:虚拟用户(系统账户)
const [systemUsers] = await db.execute(
`SELECT id FROM users
WHERE is_system_account = TRUE AND id NOT IN (${placeholders})
ORDER BY balance DESC, RAND()
LIMIT 1`,
excludeList
);
if (systemUsers.length > 0) {
return systemUsers[0].id;
}
// 如果连系统账户都没有,抛出错误
throw new Error('没有可用的匹配目标:所有符合条件的用户都被排除');
} catch (error) {
console.error('获取匹配目标失败:', error);
throw error;
}
}
// 获取可用用户
/**
* 生成智能分配金额
* 1. 排除今天打款的用户
* 2. 优先分配给负余额用户余额+待确认收款为负数
* 3. 每笔最高5000不够再分配给虚拟用户
* 4. 笔数3-10
* @param {number} totalAmount - 总金额
* @param {number} excludeUserId - 排除的用户ID发起人
* @returns {Promise<Array>} 分配金额数组
*/
async generateSmartAllocations(totalAmount, excludeUserId) {
const db = getDB();
const minAmount = 100;
const maxAmountPerTransfer = totalAmount;
const minTransfers = totalAmount > 5000 ? 4 : 3;
const maxTransfers = 10;
try {
// 获取负余额用户
let [userBalanceResult] = await db.execute(
`SELECT
u.id as user_id,
u.balance as current_balance
FROM users u
WHERE u.is_system_account = FALSE
AND u.id != ?
2025-08-28 09:14:56 +08:00
AND u.balance < -100
2025-08-26 10:06:23 +08:00
AND u.audit_status = 'approved'
ORDER BY u.balance ASC`,
[excludeUserId]
);
// 处理查询到的负余额用户
const availableUsers = [];
for (const user of userBalanceResult) {
// 确保余额是数字类型
const currentBalance = parseFloat(user.current_balance) || 0;
// 更新用户对象
user.current_balance = currentBalance;
// 查询用户的分配订单金额统计
const [orderStatusResult] = await db.execute(
`SELECT
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) as pending_amount,
SUM(CASE WHEN status = 'processing' THEN amount ELSE 0 END) as processing_amount
FROM transfers
WHERE to_user_id = ?`,
[user.user_id]
);
// 查询用户的分配订单金待确认金额统计
const [orderStatusConfirmedResult] = await db.execute(
`SELECT
SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as confirmed_amount
FROM transfers
WHERE to_user_id = ?`,
[user.user_id]
);
// 查询用户当天在matching_orders表中打出去的款项
const today = getLocalDateString();
const [todayOutflowResult] = await db.execute(
`SELECT
SUM(amount) as today_outflow
FROM matching_orders
WHERE initiator_id = ? AND DATE(updated_at) = ?`,
[user.user_id, today]
);
// 添加分配金额信息到用户对象
const orderStatus = orderStatusResult[0] || { pending_amount: 0, processing_amount: 0 };
const todayOutflow = todayOutflowResult[0] || { today_outflow: 0 };
const orderStatusConfirmed = orderStatusConfirmedResult[0] || { confirmed_amount: 0 };
user.today_outflow = parseFloat(todayOutflow.today_outflow) || 0;
user.pending_amount = parseFloat(orderStatus.pending_amount) || 0;
user.processing_amount = parseFloat(orderStatus.processing_amount) || 0;
user.confirmed_amount = parseFloat(orderStatusConfirmed.confirmed_amount) || 0;
user.has_active_allocations = user.current_balance + user.pending_amount + user.processing_amount + user.confirmed_amount + user.today_outflow;
// 所有查询到的用户都是负余额用户,直接添加到可用列表
}
userBalanceResult = userBalanceResult.sort((a, b) => a.has_active_allocations - b.has_active_allocations);
for (const user of userBalanceResult) {
if (user.has_active_allocations < -100 && maxTransfers > availableUsers.length + 1) {
if (minTransfers === 3 && availableUsers.length < 3) {
availableUsers.push(user);
}
if (minTransfers === 4) {
availableUsers.push(user);
}
}
console.log(user, '普通用户');
}
console.log(`可参与分配的负余额用户数量: ${availableUsers.length}`);
// 第二步由于第一步已经筛选了余额小于0的用户这里直接使用可用用户作为优先分配用户
// 用户已按余额升序排列(最负的优先),然后按可分配金额降序排列
const priorityUsers = availableUsers; // 所有可用用户都是负余额用户,无需再次筛选
// 第三步:获取虚拟用户作为备选
const [virtualUsersResult] = await db.execute(
`SELECT id, username, balance FROM users
WHERE is_system_account = TRUE
ORDER BY balance DESC, RAND()`
);
// 计算分配方案
const allocations = [];
let remainingAmount = totalAmount;
// 优先分配给当前余额为负的用户
for (const user of priorityUsers) {
if (remainingAmount <= 0 || allocations.length >= maxTransfers) break;
// 计算该用户可接受的最大分配金额
// 确保分配后用户余额不会变成正数
const currentBalance = Math.abs(user.has_active_allocations);
// 使用随机分配而不是平均分配
const remainingTransfers = minTransfers - allocations.length;
const minRequiredForRemaining = Math.max(0, (remainingTransfers - 1) * minAmount); // 为剩余转账预留的最小金额,确保不为负数
console.log(`用户${user.user_id}分配计算: remainingAmount=${remainingAmount}, remainingTransfers=${remainingTransfers}, minRequiredForRemaining=${minRequiredForRemaining}`);
const maxRandomAllocation = Math.min(
currentBalance, // 不能超过安全分配额度,确保接收后余额不会变成正数
maxAmountPerTransfer, // 单笔最大金额限制
remainingAmount - minRequiredForRemaining // 确保剩余金额足够分配给后续转账
);
// 生成随机分配金额,使用极度偏向大值的算法
let maxUserAllocation = 0;
if (maxRandomAllocation >= minAmount) {
const range = maxRandomAllocation - minAmount;
if (range <= 0) {
maxUserAllocation = minAmount;
} else {
2025-08-28 09:14:56 +08:00
// 使用更均匀的分配策略
const randomFactor = Math.random(); // 使用均匀分布
2025-08-26 10:06:23 +08:00
2025-08-28 09:14:56 +08:00
// 基础分配:在整个范围内更均匀分布,减少偏向性
const baseOffset = Math.floor(range * 0.15); // 降低到15%的基础偏移
2025-08-26 10:06:23 +08:00
const adjustedRange = range - baseOffset;
maxUserAllocation = Math.floor(randomFactor * adjustedRange) + minAmount + baseOffset;
2025-08-28 09:14:56 +08:00
// 进一步减少额外增量的影响
const bonusRange = Math.min(range * 0.1, maxRandomAllocation - maxUserAllocation); // 降低到10%
if (bonusRange > 0 && Math.random() > 0.7) { // 30%概率获得额外增量,进一步降低
const bonus = Math.floor(Math.random() * bonusRange * 0.3); // 使用30%的bonus范围
2025-08-26 10:06:23 +08:00
maxUserAllocation += bonus;
}
// 确保不超过最大限制
maxUserAllocation = Math.min(maxUserAllocation, maxRandomAllocation);
}
}
console.log(maxUserAllocation, minAmount, '+++++++++++++++');
if (maxUserAllocation >= minAmount) {
allocations.push({
userId: user.user_id,
username: user.username || `用户${user.user_id}`,
amount: maxUserAllocation,
userType: 'priority_user',
currentBalance: user.current_balance,
historicalNetBalance: user.historical_net_balance,
totalPendingInflow: user.total_pending_inflow,
availableForAllocation: user.available_for_allocation,
todayOutflow: user.today_outflow
});
remainingAmount -= maxUserAllocation;
}
}
// 如果还有剩余金额且分配数量不足最小笔数,最后分配给虚拟用户
const availableVirtualUsers = virtualUsersResult
// 如果需要分配给虚拟用户,使用随机分配算法
if (remainingAmount > 0 && availableVirtualUsers.length > 0) {
const maxPossibleTransfers = Math.min((minTransfers - allocations.length) <= 0 ? 1 : minTransfers - allocations.length, availableVirtualUsers.length);
// 生成随机分配金额数组
const randomAmounts = this.generateRandomAmounts(remainingAmount, maxPossibleTransfers, minAmount, maxAmountPerTransfer);
// 为每个随机金额分配虚拟用户
for (let i = 0; i < randomAmounts.length && availableVirtualUsers.length > 0; i++) {
const randomIndex = Math.floor(Math.random() * availableVirtualUsers.length);
const virtualUser = availableVirtualUsers[randomIndex];
allocations.push({
userId: virtualUser.id,
username: virtualUser.username,
amount: randomAmounts[i],
userType: 'virtual',
balance: virtualUser.balance
});
remainingAmount -= randomAmounts[i];
availableVirtualUsers.splice(randomIndex, 1);
}
}
// 检查是否有足够的用户来完成分配
if (remainingAmount > 0 && allocations.length < minTransfers && availableVirtualUsers.length === 0) {
throw new Error('没有足够的可用用户来完成分配(避免重复分配给同一用户)');
}
// 确保至少有最小笔数
if (allocations.length < minTransfers) {
throw new Error(`无法生成足够的分配:需要至少${minTransfers}笔,但只能生成${allocations.length}`);
}
// 精确控制总金额,避免超出预期
const currentTotal = allocations.reduce((sum, a) => sum + a.amount, 0);
console.log('剩余金额处理前:', remainingAmount, '当前总分配金额:', currentTotal, '期望总金额:', totalAmount);
if (remainingAmount > 0 && allocations.length > 0) {
// 检查是否会超出总金额
if (currentTotal + remainingAmount <= totalAmount) {
console.log('将剩余金额', remainingAmount, '加到最后一笔分配');
allocations[allocations.length - 1].amount += remainingAmount;
} else {
// 如果会超出,只加到刚好等于总金额的部分
const allowedAmount = totalAmount - currentTotal;
if (allowedAmount > 0) {
console.log('调整最后一笔分配,增加', allowedAmount, '元以达到精确总金额');
allocations[allocations.length - 1].amount += allowedAmount;
}
}
remainingAmount = 0; // 重置剩余金额
}
console.log(`智能分配完成: 总金额${totalAmount}元,分配${allocations.length}`);
console.log('分配详情:', allocations.map(a =>
`${a.amount}元 -> 用户${a.userId}(${a.username}) [${a.userType}]`
).join(', '));
return allocations;
} catch (error) {
console.error('智能分配失败:', error);
throw error;
}
}
/**
2025-08-28 09:14:56 +08:00
* 生成随机分配金额数组更均匀的分配策略
2025-08-26 10:06:23 +08:00
* @param {number} totalAmount - 总金额
* @param {number} transferCount - 分配笔数
* @param {number} minAmount - 最小金额
* @param {number} maxAmount - 最大金额
* @returns {number[]} 随机金额数组
*/
generateRandomAmounts(totalAmount, transferCount, minAmount, maxAmount) {
if (transferCount <= 0 || totalAmount < minAmount * transferCount) {
return [];
}
2025-08-28 09:14:56 +08:00
// 使用更均匀的分配策略
2025-08-26 10:06:23 +08:00
const amounts = [];
2025-08-28 09:14:56 +08:00
2025-08-26 10:06:23 +08:00
// 首先为每笔分配最小金额
for (let i = 0; i < transferCount; i++) {
amounts.push(minAmount);
}
2025-08-28 09:14:56 +08:00
2025-08-26 10:06:23 +08:00
let remainingToDistribute = totalAmount - (minAmount * transferCount);
2025-08-28 09:14:56 +08:00
// 计算平均每笔应该额外分配的金额
const averageExtra = Math.floor(remainingToDistribute / transferCount);
// 为每笔添加平均额外金额,但加入一些随机性
for (let i = 0; i < transferCount && remainingToDistribute > 0; i++) {
// 计算这笔最多还能增加多少不超过maxAmount
const maxPossibleIncrease = Math.min(
maxAmount - amounts[i],
remainingToDistribute
);
if (maxPossibleIncrease > 0) {
// 在平均值附近随机分配,但控制在更小的范围内以保证更均匀
const baseIncrease = Math.min(averageExtra, maxPossibleIncrease);
const randomVariation = Math.floor(baseIncrease * 0.15); // 减少到15%的随机变化
const minIncrease = Math.max(0, baseIncrease - randomVariation);
const maxIncrease = Math.min(maxPossibleIncrease, baseIncrease + randomVariation);
const increase = Math.floor(Math.random() * (maxIncrease - minIncrease + 1)) + minIncrease;
amounts[i] += increase;
remainingToDistribute -= increase;
}
}
// 如果还有剩余金额,尽量均匀分配给还能接受的笔数
2025-08-26 10:06:23 +08:00
while (remainingToDistribute > 0) {
const availableIndices = [];
for (let i = 0; i < transferCount; i++) {
if (amounts[i] < maxAmount) {
availableIndices.push(i);
}
}
2025-08-28 09:14:56 +08:00
2025-08-26 10:06:23 +08:00
if (availableIndices.length === 0) {
2025-08-28 09:14:56 +08:00
break; // 无法继续分配
2025-08-26 10:06:23 +08:00
}
2025-08-28 09:14:56 +08:00
// 计算每个可用位置应该分配多少
const perIndexAmount = Math.floor(remainingToDistribute / availableIndices.length);
const remainder = remainingToDistribute % availableIndices.length;
// 为每个可用位置分配相等的金额
for (let i = 0; i < availableIndices.length && remainingToDistribute > 0; i++) {
const index = availableIndices[i];
const maxIncrease = Math.min(maxAmount - amounts[index], remainingToDistribute);
if (maxIncrease > 0) {
// 基础分配金额
let increase = Math.min(perIndexAmount, maxIncrease);
// 如果是前几个位置,额外分配余数
if (i < remainder) {
increase = Math.min(increase + 1, maxIncrease);
}
amounts[index] += increase;
remainingToDistribute -= increase;
}
}
// 如果所有位置都已达到最大值,退出循环
if (perIndexAmount === 0 && remainder === 0) {
break;
2025-08-26 10:06:23 +08:00
}
}
2025-08-28 09:14:56 +08:00
// 如果还有剩余金额无法分配,返回空数组表示失败
2025-08-26 10:06:23 +08:00
if (remainingToDistribute > 0) {
return [];
}
2025-08-28 09:14:56 +08:00
2025-08-26 10:06:23 +08:00
return amounts;
}
// 生成3笔随机金额总计指定金额保留原方法用于兼容性
generateThreeRandomAmounts(totalAmount) {
// 确保总金额足够分配三笔最小金额
const minAmount = 500;
const maxAmount = Math.min(5000, totalAmount - 2 * minAmount);
// 生成第一笔金额 (500-5000)
const amount1 = Math.floor(Math.random() * (maxAmount - minAmount + 1)) + minAmount;
// 生成第二笔金额 (500-剩余金额-500)
const remaining1 = totalAmount - amount1;
const maxAmount2 = Math.min(5000, remaining1 - minAmount);
const amount2 = Math.floor(Math.random() * (maxAmount2 - minAmount + 1)) + minAmount;
// 第三笔是剩余金额
const amount3 = totalAmount - amount1 - amount2;
return [amount1, amount2, amount3];
}
// 生成随机金额(保留原方法用于其他地方)
/**
* 确认分配并创建转账记录
* @param {number} allocationId - 分配ID
* @param {number} userId - 用户ID
* @param {number} transferAmount - 实际转账金额用于校验
* @param {string} description - 转账描述
* @param {string} voucher - 转账凭证URL
* @returns {number} 转账记录ID
*/
async confirmAllocation(allocationId, userId, transferAmount = null, description = null, voucher = null) {
const db = getDB();
try {
await db.query('START TRANSACTION');
// 获取分配信息
const [allocations] = await db.execute(
'SELECT * FROM transfers WHERE id = ? AND from_user_id = ?',
[allocationId, userId]
);
if (allocations.length === 0) {
throw new Error('分配不存在或无权限');
}
const allocation = allocations[0];
// 校验转账金额(如果提供了转账金额)
if (transferAmount !== null) {
const expectedAmount = parseFloat(allocation.amount);
const actualAmount = parseFloat(transferAmount);
if (Math.abs(expectedAmount - actualAmount) > 0.01) {
throw new Error(`转账金额不匹配!应转账 ${expectedAmount} 元,实际转账 ${actualAmount}`);
}
}
// 检查分配状态
if (allocation.status !== 'pending') {
throw new Error('该分配已处理,无法重复确认');
}
// 检查匹配订单是否已超时
const [matchingOrder] = await db.execute(
'SELECT * FROM matching_orders WHERE id = ?',
[allocation.matching_order_id]
);
if (matchingOrder.length === 0) {
throw new Error('匹配订单不存在');
}
// 检查订单是否已被取消(超时会导致订单被取消)
if (matchingOrder[0].status === 'cancelled') {
throw new Error('该匹配订单已超时取消,无法进行转账');
}
// 检查是否存在相关的超时转账记录
const [timeoutTransfers] = await db.execute(
`SELECT COUNT(*) as count FROM transfers
WHERE (from_user_id = ? OR to_user_id = ?)
AND is_overdue = 1
AND description LIKE ?`,
[userId, userId, `%匹配订单 ${allocation.matching_order_id}%`]
);
if (timeoutTransfers[0].count > 0) {
throw new Error('该匹配订单存在超时记录,无法继续转账。请联系管理员处理');
}
// 计算3小时后的截止时间
const deadline = dayjs().add(3, 'hour').toDate();
// 更新转账记录状态为confirmed跳过待确认环节
const transferDescription = description || `匹配订单 ${allocation.matching_order_id}${allocation.cycle_number} 轮转账`;
const [transferResult] = await db.execute(
`UPDATE transfers
SET status = "confirmed", description = ?, deadline_at = ?, confirmed_at = NOW(), voucher_url = ?
WHERE id = ?`,
[
transferDescription,
deadline,
voucher,
allocationId
]
);
// 注意:发送方余额将在接收方确认收款时扣除,而不是在确认转账时扣除
// 这样可以避免资金被锁定但收款方未确认的情况
// 记录确认动作
await db.execute(
'INSERT INTO matching_records (matching_order_id, user_id, action, amount, note) VALUES (?, ?, "confirm", ?, ?)',
[
allocation.matching_order_id,
userId,
allocation.amount,
transferAmount ? `实际转账金额: ${transferAmount}` : null
]
);
await db.query('COMMIT');
// 检查是否需要进入下一轮
await this.checkCycleCompletion(allocation.matching_order_id, allocation.cycle_number);
return transferResult.insertId;
} catch (error) {
await db.query('ROLLBACK');
throw error;
}
}
// 检查轮次完成情况
async checkCycleCompletion(matchingOrderId, cycleNumber) {
const db = getDB();
try {
// 检查当前轮次是否全部确认
const [pending] = await db.execute(
'SELECT COUNT(*) as count FROM transfers WHERE matching_order_id = ? AND cycle_number = ? AND status = "pending"',
[matchingOrderId, cycleNumber]
);
if (pending[0].count === 0) {
// 当前轮次完成,检查是否需要下一轮
const [order] = await db.execute(
'SELECT * FROM matching_orders WHERE id = ?',
[matchingOrderId]
);
const currentOrder = order[0];
if (currentOrder.cycle_count + 1 < currentOrder.max_cycles) {
// 开始下一轮
await db.execute(
'UPDATE matching_orders SET cycle_count = cycle_count + 1 WHERE id = ?',
[matchingOrderId]
);
// 生成下一轮分配
const amounts = this.generateThreeRandomAmounts(currentOrder.amount);
await this.generateThreeAllocations(matchingOrderId, amounts, currentOrder.initiator_id);
} else {
// 完成所有轮次
await db.execute(
'UPDATE matching_orders SET status = "completed" WHERE id = ?',
[matchingOrderId]
);
console.log(`匹配订单 ${matchingOrderId} 已完成所有轮次`);
// 检查用户是否完成第三次匹配,如果是则给代理分佣
await this.checkAndProcessAgentCommission(currentOrder.initiator_id);
}
}
} catch (error) {
console.error('检查轮次完成情况失败:', error);
throw error;
}
}
// 获取用户的匹配订单
async getUserMatchingOrders(userId, page = 1, limit = 10) {
const db = getDB();
const offset = (parseInt(page) - 1) * parseInt(limit);
try {
// 获取用户发起的订单
const [orders] = await db.execute(
`SELECT mo.*, u.username as initiator_name,u.real_name as initiator_real_name
FROM matching_orders mo
JOIN users u ON mo.initiator_id = u.id
WHERE mo.initiator_id = ?
ORDER BY mo.created_at DESC
LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)}`,
[userId]
);
// 同时获取系统反向匹配订单(如果用户参与了分配)
const [systemOrders] = await db.execute(
`SELECT DISTINCT mo.*, u.username as initiator_name
FROM matching_orders mo
JOIN users u ON mo.initiator_id = u.id
JOIN transfers oa ON mo.id = oa.id
WHERE mo.is_system_reverse = TRUE AND oa.to_user_id = ?
ORDER BY mo.created_at DESC
LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)}`,
[userId]
);
// 合并订单列表
const allOrders = [...orders, ...systemOrders];
// 按创建时间排序
allOrders.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
// 为每个订单获取分配信息
for (let order of allOrders) {
const [allocations] = await db.execute(
`SELECT * FROM transfers WHERE matching_order_id = ? ORDER BY cycle_number, created_at`,
[order.id]
);
order.allocations = allocations;
}
return allOrders;
} catch (error) {
console.error('获取用户匹配订单失败:', error);
throw error;
}
}
// 获取用户待处理的分配
async getUserPendingAllocations(userId) {
const db = getDB();
try {
const [allocations] = await db.execute(
`(SELECT oa.*, mo.amount as total_amount, mo.status as order_status, u.username as to_user_name,
u.real_name as to_user_real_name,
DATE_ADD(oa.created_at, INTERVAL 150 MINUTE) as expected_deadline,
oa.outbound_date,
oa.return_date,
oa.can_return_after,
oa.confirmed_at
FROM transfers oa
JOIN matching_orders mo ON oa.matching_order_id = mo.id
JOIN users u ON oa.to_user_id = u.id
WHERE oa.from_user_id = ? AND oa.status = "pending" AND mo.status != "cancelled"
AND (oa.source_type IS NULL))
UNION ALL
(SELECT oa.*, oa.amount as total_amount, 'active' as order_status, u.username as to_user_name,
u.real_name as to_user_real_name,
DATE_ADD(oa.created_at, INTERVAL 150 MINUTE) as expected_deadline,
oa.outbound_date,
oa.return_date,
oa.can_return_after,
oa.confirmed_at
FROM transfers oa
JOIN users u ON oa.to_user_id = u.id
WHERE oa.from_user_id = ? AND oa.status = "pending")
ORDER BY created_at ASC`,
[userId, userId]
);
// 检查每个分配的超时状态,但不过滤掉
const allocationsWithTimeoutStatus = [];
for (const allocation of allocations) {
// 检查是否存在相关的超时转账记录
const [timeoutTransfers] = await db.execute(
`SELECT COUNT(*) as count FROM transfers
WHERE (from_user_id = ? OR to_user_id = ?)
AND is_overdue = 1
AND description LIKE ?`,
[userId, userId, `%匹配订单 ${allocation.matching_order_id}%`]
);
// 添加超时状态标记
allocation.has_timeout_record = timeoutTransfers[0].count > 0;
allocationsWithTimeoutStatus.push(allocation);
}
// 检查并处理超时订单
const now = new Date();
// 隐藏系统账户身份并添加时效状态
const processedAllocations = allocationsWithTimeoutStatus.map(allocation => {
const deadline = allocation.transfer_deadline || allocation.expected_deadline;
const deadlineDate = new Date(deadline);
const timeLeft = deadlineDate - now;
// 计算剩余时间
let timeStatus = 'normal';
let timeLeftText = '';
if (timeLeft <= 0) {
timeStatus = 'expired';
timeLeftText = '已超时';
} else if (timeLeft <= 2.5 * 60 * 60 * 1000) { // 2.5小时内
timeStatus = 'urgent';
const hours = Math.floor(timeLeft / (60 * 60 * 1000));
const minutes = Math.floor((timeLeft % (60 * 60 * 1000)) / (60 * 1000));
timeLeftText = hours > 0 ? `${hours}小时${minutes}分钟` : `${minutes}分钟`;
} else {
const hours = Math.floor(timeLeft / (60 * 60 * 1000));
const minutes = Math.floor((timeLeft % (60 * 60 * 1000)) / (60 * 1000));
timeLeftText = `${hours}小时${minutes}分钟`;
}
return {
...allocation,
to_user_name: allocation.to_user_name || '匿名用户',
is_system_account: undefined, // 移除系统账户标识
deadline: deadline,
time_status: timeStatus,
time_left: timeLeftText,
can_transfer: !allocation.has_timeout_record, // 是否可以转账
timeout_reason: allocation.has_timeout_record ? '该匹配订单存在超时记录,无法继续转账' : null
};
});
return processedAllocations;
} catch (error) {
console.error('获取用户待处理分配失败:', error);
throw error;
}
}
// 检查并处理代理佣金
async checkAndProcessAgentCommission(userId) {
const db = getDB();
try {
// 检查用户是否有代理关系
const [agentRelation] = await db.execute(
'SELECT agent_id, created_at FROM agent_merchants WHERE merchant_id = ?',
[userId]
);
if (agentRelation.length === 0) {
return; // 用户没有代理,无需处理
}
const agentId = agentRelation[0].agent_id;
const agentJoinTime = agentRelation[0].created_at;
// 检查用户给他人转账的次数(状态为已收款,且转账时间在代理商入驻之后)
const [completedTransfers] = await db.execute(
'SELECT COUNT(*) as count FROM transfers WHERE from_user_id = ? AND status = "received" AND created_at >= ?',
[userId, agentJoinTime]
);
const transferCount = completedTransfers[0].count;
// 如果完成至少三次转账,给代理分佣
if (transferCount >= 3) {
// 检查是否已经给过佣金(防止重复分佣)
const [existingCommission] = await db.execute(
'SELECT id FROM agent_commission_records WHERE agent_id = ? AND merchant_id = ? AND description LIKE "%第三次转账%"',
[agentId, userId]
);
if (existingCommission.length === 0) {
// 计算佣金399元的10% = 39.9元
const commissionAmount = 399 * 0.10;
// 记录佣金
await db.execute(
'INSERT INTO agent_commission_records (agent_id, merchant_id, commission_amount, commission_type, description, created_at) VALUES (?, ?, ?, "matching", "用户完成第三次转账获得的代理佣金", NOW())',
[agentId, userId, commissionAmount]
);
console.log(`用户 ${userId} 完成第三次转账,为代理 ${agentId} 分佣 ${commissionAmount}`);
}
}
} catch (error) {
console.error('处理代理佣金失败:', error);
// 不抛出错误,避免影响主流程
}
}
}
module.exports = new MatchingService();