1555 lines
57 KiB
JavaScript
1555 lines
57 KiB
JavaScript
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 < 3000 || customAmount > 50000) {
|
||
throw new Error('大额匹配金额必须在3000-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 {
|
||
// 首先获取当前用户的城市、省份和区域信息
|
||
const [currentUserResult] = await db.execute(
|
||
`SELECT city, province, district_id FROM users WHERE id = ?`,
|
||
[excludeUserId]
|
||
);
|
||
|
||
const currentUserCity = currentUserResult[0]?.city;
|
||
const currentUserProvince = currentUserResult[0]?.province;
|
||
const currentUserDistrictId = currentUserResult[0]?.district_id;
|
||
|
||
// 获取负余额用户,按区县、城市、省份优先级排序
|
||
let [userBalanceResult] = await db.execute(
|
||
`SELECT
|
||
u.id as user_id,
|
||
u.balance as current_balance,
|
||
u.city,
|
||
u.province,
|
||
u.district_id
|
||
FROM users u
|
||
WHERE u.is_system_account = FALSE
|
||
AND u.is_distribute = TRUE
|
||
AND u.id != ?
|
||
AND u.balance < -100
|
||
AND u.audit_status = 'approved'
|
||
AND u.user_type != 'directly_operated'
|
||
AND u.payment_status = 'paid'
|
||
ORDER BY
|
||
CASE
|
||
WHEN u.city = ? AND u.district_id = ? THEN 1 -- 相同城市且相同区县排第一
|
||
WHEN u.city = ? THEN 2 -- 相同城市但不同区县排第二
|
||
WHEN u.province = ? THEN 3 -- 相同省份但不同城市排第三
|
||
ELSE 4 -- 其他省份排第四
|
||
END,
|
||
u.balance ASC`,
|
||
[excludeUserId, currentUserCity, currentUserDistrictId, currentUserCity, currentUserProvince]
|
||
);
|
||
|
||
// 处理查询到的负余额用户
|
||
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
|
||
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]
|
||
);
|
||
//查询用户给其他用户已确认的金额统计(要减去,因为款项还没回来)
|
||
const [orderStatusConfirmedResultFrom] = await db.execute(
|
||
`SELECT
|
||
SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as confirmed_amount
|
||
FROM transfers
|
||
WHERE from_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 };
|
||
const todayOutflow = todayOutflowResult[0] || { today_outflow: 0 };
|
||
const orderStatusConfirmedFrom = orderStatusConfirmedResultFrom[0] || { confirmed_amount: 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.confirmed_amount = parseFloat(orderStatusConfirmed.confirmed_amount) || 0;
|
||
user.has_active_allocations = user.current_balance + user.pending_amount + user.confirmed_amount + user.today_outflow - orderStatusConfirmedFrom.confirmed_amount;
|
||
|
||
|
||
|
||
// 所有查询到的用户都是负余额用户,直接添加到可用列表
|
||
}
|
||
userBalanceResult = userBalanceResult.filter(user => user.has_active_allocations < -100);
|
||
userBalanceResult = userBalanceResult.sort((a, b) => a.has_active_allocations - b.has_active_allocations);
|
||
for (const user of userBalanceResult) {
|
||
if (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 {
|
||
if (maxRandomAllocation > 1000) {
|
||
// 使用更均匀的分配策略
|
||
const randomFactor = Math.random(); // 使用均匀分布
|
||
|
||
// 基础分配:在整个范围内更均匀分布,减少偏向性
|
||
const baseOffset = Math.floor(range * 0.15); // 降低到15%的基础偏移
|
||
const adjustedRange = range - baseOffset;
|
||
maxUserAllocation = Math.floor(randomFactor * adjustedRange) + minAmount + baseOffset;
|
||
|
||
// 进一步减少额外增量的影响
|
||
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范围
|
||
maxUserAllocation += bonus;
|
||
}
|
||
}else{
|
||
maxUserAllocation = maxRandomAllocation
|
||
}
|
||
|
||
|
||
// 确保不超过最大限制
|
||
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,
|
||
has_active_allocations: user.has_active_allocations
|
||
});
|
||
remainingAmount -= maxUserAllocation;
|
||
}
|
||
}
|
||
|
||
// 如果还有剩余金额且分配数量不足最小笔数,最后分配给虚拟用户
|
||
const availableVirtualUsers = virtualUsersResult
|
||
|
||
// 如果有剩余金额,优先检查现有非虚拟用户是否还能消化
|
||
if (remainingAmount > 0) {
|
||
// 筛选出非虚拟用户分配记录
|
||
|
||
if (allocations.length > 0) {
|
||
let totalAvailableCapacity = 0;
|
||
const userCapacities = [];
|
||
|
||
// 计算每个用户的剩余可分配容量
|
||
for (const allocation of allocations) {
|
||
// 获取用户当前的实际余额状态(使用has_active_allocations作为实际可分配余额)
|
||
const maxSafeAmount = Math.abs(allocation.has_active_allocations);
|
||
const remainingCapacity = maxSafeAmount - allocation.amount;
|
||
|
||
if (remainingCapacity > 0) {
|
||
userCapacities.push({
|
||
allocation,
|
||
capacity: remainingCapacity
|
||
});
|
||
totalAvailableCapacity += remainingCapacity;
|
||
}
|
||
}
|
||
|
||
console.log(`现有用户剩余容量: ${totalAvailableCapacity}, 待分配金额: ${remainingAmount}`);
|
||
|
||
// 如果现有用户能够消化剩余金额
|
||
if (totalAvailableCapacity >= remainingAmount && userCapacities.length > 0 && allocations.length >= 3) {
|
||
// 按平均分配给这些用户,但需要检查每个用户的分配上限
|
||
const averageAmount = Math.floor(remainingAmount / userCapacities.length);
|
||
let distributedAmount = 0;
|
||
let remainingToDistribute = remainingAmount;
|
||
|
||
for (let i = 0; i < userCapacities.length; i++) {
|
||
const { allocation, capacity } = userCapacities[i];
|
||
|
||
// 计算本次可分配的金额
|
||
let amountToAdd = 0;
|
||
|
||
if (i === userCapacities.length - 1) {
|
||
// 最后一个用户分配剩余的所有金额,但不能超过其容量
|
||
amountToAdd = Math.min(remainingToDistribute, capacity);
|
||
} else {
|
||
// 其他用户按平均分配,但不能超过其容量
|
||
amountToAdd = Math.min(averageAmount, capacity);
|
||
}
|
||
|
||
if (amountToAdd > 0) {
|
||
allocation.amount += amountToAdd;
|
||
distributedAmount += amountToAdd;
|
||
remainingToDistribute -= amountToAdd;
|
||
console.log(`为用户${allocation.userId}追加分配${amountToAdd}元,总分配${allocation.amount}元,剩余容量${capacity - amountToAdd}元`);
|
||
}
|
||
}
|
||
|
||
// 更新实际分配的剩余金额
|
||
remainingAmount = remainingToDistribute;
|
||
|
||
if (remainingAmount === 0) {
|
||
console.log('剩余金额已全部分配给现有用户');
|
||
} else {
|
||
console.log(`部分剩余金额已分配给现有用户,仍有${remainingAmount}元未分配`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果仍有剩余金额,检查是否有未分配的用户可以消化剩余金额
|
||
if (remainingAmount > 0) {
|
||
// 获取已分配的用户ID列表
|
||
const allocatedUserIds = new Set(allocations.map(a => a.userId));
|
||
|
||
// 从原始用户列表中找到未分配的用户
|
||
const unallocatedUsers = userBalanceResult.filter(user => !allocatedUserIds.has(user.user_id));
|
||
|
||
if (unallocatedUsers.length > 0) {
|
||
console.log(`发现${unallocatedUsers.length}个未分配的用户,剩余金额: ${remainingAmount}`);
|
||
|
||
// 查找可分配金额大于剩余金额的用户
|
||
for (const user of unallocatedUsers) {
|
||
const maxSafeAmount = Math.abs(user.has_active_allocations);
|
||
|
||
if (maxSafeAmount >= remainingAmount) {
|
||
// 找到合适的用户,分配剩余金额
|
||
allocations.push({
|
||
userId: user.user_id,
|
||
username: user.username || `User${user.user_id}`,
|
||
amount: remainingAmount,
|
||
userType: 'priority_user',
|
||
currentBalance: user.current_balance,
|
||
availableForAllocation: user.has_active_allocations
|
||
});
|
||
|
||
console.log(`为未分配用户${user.user_id}分配剩余金额${remainingAmount}元`);
|
||
remainingAmount = 0;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果仍有剩余金额,分配给虚拟用户
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成随机分配金额数组(更均匀的分配策略)
|
||
* @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 [];
|
||
}
|
||
|
||
// 使用更均匀的分配策略
|
||
const amounts = [];
|
||
|
||
// 首先为每笔分配最小金额
|
||
for (let i = 0; i < transferCount; i++) {
|
||
amounts.push(minAmount);
|
||
}
|
||
|
||
let remainingToDistribute = totalAmount - (minAmount * transferCount);
|
||
|
||
// 计算平均每笔应该额外分配的金额
|
||
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;
|
||
}
|
||
}
|
||
|
||
// 如果还有剩余金额,尽量均匀分配给还能接受的笔数
|
||
while (remainingToDistribute > 0) {
|
||
const availableIndices = [];
|
||
for (let i = 0; i < transferCount; i++) {
|
||
if (amounts[i] < maxAmount) {
|
||
availableIndices.push(i);
|
||
}
|
||
}
|
||
|
||
if (availableIndices.length === 0) {
|
||
break; // 无法继续分配
|
||
}
|
||
|
||
// 计算每个可用位置应该分配多少
|
||
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;
|
||
}
|
||
}
|
||
|
||
// 如果还有剩余金额无法分配,返回空数组表示失败
|
||
if (remainingToDistribute > 0) {
|
||
return [];
|
||
}
|
||
|
||
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(); |