const express = require('express'); const router = express.Router(); const db = require('../database'); const { auth, adminAuth } = require('../middleware/auth'); const logger = require('../config/logger'); const matchingService = require('../services/matchingService'); const dayjs = require('dayjs'); /** * @swagger * tags: * name: MatchingAdmin * description: 匹配订单管理员相关接口 */ /** * @swagger * components: * schemas: * UnreasonableMatch: * type: object * properties: * allocation_id: * type: integer * description: 分配ID * from_user_id: * type: integer * description: 发送方用户ID * to_user_id: * type: integer * description: 接收方用户ID * amount: * type: number * description: 分配金额 * status: * type: string * enum: [pending, confirmed, rejected, cancelled] * description: 分配状态 * to_username: * type: string * description: 接收方用户名 * to_user_balance: * type: number * description: 接收方用户余额 * from_username: * type: string * description: 发送方用户名 * from_user_balance: * type: number * description: 发送方用户余额 */ /** * @swagger * /api/matching-admin/unreasonable-matches: * get: * summary: 获取不合理的匹配记录(正余额用户被匹配的情况) * tags: [MatchingAdmin] * security: * - bearerAuth: [] * parameters: * - in: query * name: page * schema: * type: integer * default: 1 * description: 页码 * - in: query * name: limit * schema: * type: integer * default: 20 * description: 每页数量 * responses: * 200: * description: 成功获取不合理匹配记录 * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * data: * type: object * properties: * matches: * type: array * items: * $ref: '#/components/schemas/UnreasonableMatch' * pagination: * type: object * properties: * page: * type: integer * limit: * type: integer * total: * type: integer * totalPages: * type: integer * 401: * description: 未授权 * 403: * description: 无管理员权限 * 500: * description: 服务器错误 */ router.get('/unreasonable-matches', auth, adminAuth, async (req, res) => { try { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 20; const offset = (page - 1) * limit; // 查找正余额用户被匹配的情况 const query = `SELECT oa.id as allocation_id, oa.from_user_id, oa.to_user_id, oa.amount, oa.status, oa.outbound_date, oa.created_at, u_to.username as to_username, u_to.balance as to_user_balance, u_from.username as from_username, u_from.balance as from_user_balance, mo.amount as total_order_amount FROM transfers oa JOIN users u_to ON oa.to_user_id = u_to.id JOIN users u_from ON oa.from_user_id = u_from.id JOIN matching_orders mo ON oa.id = mo.id WHERE oa.source_type = 'allocation' AND u_to.balance > 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') ORDER BY oa.created_at DESC LIMIT ${offset}, ${limit}`; const countQuery = `SELECT COUNT(*) as total FROM transfers oa JOIN users u_to ON oa.to_user_id = u_to.id WHERE oa.source_type = 'allocation' AND u_to.balance > 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed')`; const unreasonableMatches = await db.executeQuery(query); // 获取总数 const countResult = await db.executeQuery(countQuery); const total = countResult[0].total; res.json({ success: true, data: { matches: unreasonableMatches, pagination: { page, limit, total, totalPages: Math.ceil(total / limit) } } }); } catch (error) { console.error('获取不合理匹配记录失败:', error); res.status(500).json({ message: '获取不合理匹配记录失败' }); } }); /** * @swagger * /api/matching-admin/fix-unreasonable-match/{allocationId}: * post: * summary: 修复不合理的匹配记录 * tags: [MatchingAdmin] * security: * - bearerAuth: [] * parameters: * - in: path * name: allocationId * required: true * schema: * type: integer * description: 分配ID * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * action: * type: string * enum: [cancel, reassign] * description: 修复操作类型(取消或重新分配) * required: * - action * responses: * 200: * description: 修复成功 * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * message: * type: string * 400: * description: 参数错误或无需修复 * 401: * description: 未授权 * 403: * description: 无管理员权限 * 404: * description: 分配记录不存在 * 500: * description: 服务器错误 */ router.post('/fix-unreasonable-match/:allocationId', auth, adminAuth, async (req, res) => { try { const { allocationId } = req.params; const { action } = req.body; // 'cancel' 或 'reassign' const connection = await db.getDB().getConnection(); await connection.query('START TRANSACTION'); try { // 获取分配详情 const [allocationResult] = await connection.execute( `SELECT oa.*, u_to.balance as to_user_balance, u_to.username as to_username FROM transfers oa JOIN users u_to ON oa.to_user_id = u_to.id WHERE oa.source_type = 'allocation' AND oa.id = ?`, [allocationId] ); if (allocationResult.length === 0) { await connection.query('ROLLBACK'); connection.release(); return res.status(404).json({ message: '分配记录不存在' }); } const allocation = allocationResult[0]; if (allocation.to_user_balance <= 0) { await connection.query('ROLLBACK'); connection.release(); return res.status(400).json({ message: '该用户余额已为负数,无需修复' }); } if (action === 'cancel') { // 获取当前时间 const currentTime = new Date(); // 取消分配 await connection.execute( 'UPDATE transfers SET status = "cancelled", updated_at = ? WHERE id = ?', [currentTime, allocationId] ); // 记录操作日志 await connection.execute( 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "fix_matching", "allocation", ?, ?, ?)', [req.user.id, allocationId, `取消不合理匹配:正余额用户${allocation.to_username}(余额${allocation.to_user_balance}元)被匹配${allocation.amount}元`, currentTime] ); } else if (action === 'reassign') { // 重新分配给负余额用户 const usedTargetUsers = new Set([allocation.to_user_id]); const newTargetUser = await matchingService.getMatchingTargetExcluding(allocation.from_user_id, usedTargetUsers); if (!newTargetUser) { await connection.query('ROLLBACK'); connection.release(); return res.status(400).json({ message: '没有可用的负余额用户进行重新分配' }); } // 获取当前时间 const currentTime = new Date(); // 更新分配目标 await connection.execute( 'UPDATE transfers SET to_user_id = ?, updated_at = ? WHERE id = ?', [newTargetUser, currentTime, allocationId] ); // 获取新目标用户信息 const [newUserResult] = await connection.execute( 'SELECT username, balance FROM users WHERE id = ?', [newTargetUser] ); const newUser = newUserResult[0]; // 记录操作日志 await connection.execute( 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "fix_matching", "allocation", ?, ?, ?)', [req.user.id, allocationId, `修复不合理匹配:从正余额用户${allocation.to_username}(余额${allocation.to_user_balance}元)重新分配给负余额用户${newUser.username}(余额${newUser.balance}元)`, currentTime] ); } else { await connection.query('ROLLBACK'); connection.release(); return res.status(400).json({ message: '无效的操作类型' }); } await connection.query('COMMIT'); connection.release(); res.json({ success: true, message: action === 'cancel' ? '已取消不合理匹配' : '已重新分配给负余额用户' }); } catch (innerError) { await connection.query('ROLLBACK'); connection.release(); throw innerError; } } catch (error) { console.error('修复不合理匹配失败:', error); res.status(500).json({ message: error.message || '修复不合理匹配失败' }); } }); /** * @swagger * /api/matching-admin/matching-stats: * get: * summary: 获取匹配统计信息 * tags: [MatchingAdmin] * security: * - bearerAuth: [] * responses: * 200: * description: 成功获取匹配统计信息 * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * data: * type: object * properties: * currentStats: * type: object * properties: * unreasonable_matches: * type: integer * reasonable_matches: * type: integer * system_matches: * type: integer * unreasonable_amount: * type: number * reasonable_amount: * type: number * yesterdayStats: * type: object * properties: * total_outbound: * type: number * unique_amounts: * type: integer * 401: * description: 未授权 * 403: * description: 无管理员权限 * 500: * description: 服务器错误 */ router.get('/matching-stats', auth, adminAuth, async (req, res) => { try { // 获取各种统计数据 const stats = await db.executeQuery( `SELECT COUNT(CASE WHEN u_to.balance > 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') THEN 1 END) as unreasonable_matches, COUNT(CASE WHEN u_to.balance < 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') THEN 1 END) as reasonable_matches, COUNT(CASE WHEN u_to.is_system_account = TRUE AND oa.status IN ('pending', 'confirmed') THEN 1 END) as system_matches, SUM(CASE WHEN u_to.balance > 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') THEN oa.amount ELSE 0 END) as unreasonable_amount, SUM(CASE WHEN u_to.balance < 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') THEN oa.amount ELSE 0 END) as reasonable_amount FROM transfers oa JOIN users u_to ON oa.to_user_id = u_to.id WHERE oa.source_type = 'allocation'` ); // 获取昨天的匹配验证统计 const yesterdayStr = dayjs().subtract(1, 'day').format('YYYY-MM-DD'); const yesterdayStats = await db.executeQuery( `SELECT SUM(oa.amount) as total_outbound, COUNT(DISTINCT oa.amount) as unique_amounts FROM transfers oa JOIN users u ON oa.from_user_id = u.id WHERE oa.source_type = 'allocation' AND DATE(oa.outbound_date) = ? AND oa.status = 'confirmed' AND u.is_system_account = FALSE`, [yesterdayStr] ); res.json({ success: true, data: { currentStats: stats[0], yesterdayStats: yesterdayStats[0] } }); } catch (error) { console.error('获取匹配统计失败:', error); res.status(500).json({ message: '获取匹配统计失败' }); } }); /** * @swagger * /api/matching-admin/fix-all-unreasonable: * post: * summary: 批量修复所有不合理匹配 * tags: [MatchingAdmin] * security: * - bearerAuth: [] * responses: * 200: * description: 批量修复完成 * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * message: * type: string * data: * type: object * properties: * fixedCount: * type: integer * description: 成功修复的记录数 * errorCount: * type: integer * description: 修复失败的记录数 * errors: * type: array * items: * type: string * description: 错误信息列表(最多10条) * 401: * description: 未授权 * 403: * description: 无管理员权限 * 500: * description: 服务器错误 */ router.post('/fix-all-unreasonable', auth, adminAuth, async (req, res) => { try { let fixedCount = 0; let errorCount = 0; const errors = []; // 获取所有不合理的匹配记录 const unreasonableMatches = await db.executeQuery( `SELECT oa.id, oa.from_user_id, oa.to_user_id, oa.amount, u_to.username, u_to.balance FROM transfers oa JOIN users u_to ON oa.to_user_id = u_to.id WHERE oa.source_type = 'allocation' AND u_to.balance > 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') ORDER BY u_to.balance DESC` ); for (const match of unreasonableMatches) { const connection = await db.getDB().getConnection(); try { await connection.query('START TRANSACTION'); // 尝试重新分配给负余额用户 const usedTargetUsers = new Set([match.to_user_id]); const newTargetUser = await matchingService.getMatchingTargetExcluding(match.from_user_id, usedTargetUsers); // 获取当前时间 const currentTime = new Date(); if (newTargetUser) { // 更新分配目标 await connection.execute( 'UPDATE transfers SET to_user_id = ?, updated_at = ? WHERE id = ?', [newTargetUser, currentTime, match.id] ); // 记录操作日志 await connection.execute( 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "batch_fix_matching", "allocation", ?, ?, ?)', [req.user.id, match.id, `批量修复:从正余额用户${match.username}(余额${match.balance}元)重新分配${match.amount}元给负余额用户`, currentTime] ); fixedCount++; } else { // 如果没有可用的负余额用户,取消分配 await connection.execute( 'UPDATE transfers SET status = "cancelled", updated_at = ? WHERE id = ?', [currentTime, match.id] ); await connection.execute( 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "batch_fix_matching", "allocation", ?, ?, ?)', [req.user.id, match.id, `批量修复:取消正余额用户${match.username}(余额${match.balance}元)的${match.amount}元分配`, currentTime] ); fixedCount++; } await connection.query('COMMIT'); connection.release(); } catch (error) { await connection.query('ROLLBACK'); connection.release(); errorCount++; errors.push(`分配ID ${match.id}: ${error.message}`); console.error(`修复分配${match.id}失败:`, error); } } res.json({ success: true, message: `批量修复完成:成功修复${fixedCount}条记录,失败${errorCount}条记录`, data: { fixedCount, errorCount, errors: errors.slice(0, 10) // 只返回前10个错误 } }); } catch (error) { console.error('批量修复不合理匹配失败:', error); res.status(500).json({ message: '批量修复不合理匹配失败' }); } }); /** * @swagger * /api/matching-admin/confirm-allocation/{allocationId}: * post: * summary: 管理员确认分配 * tags: [MatchingAdmin] * security: * - bearerAuth: [] * parameters: * - in: path * name: allocationId * required: true * schema: * type: integer * description: 分配ID * responses: * 200: * description: 分配确认成功 * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * message: * type: string * 401: * description: 未授权 * 403: * description: 无管理员权限 * 404: * description: 分配不存在或状态不是待处理 * 500: * description: 服务器错误 */ router.post('/confirm-allocation/:allocationId', auth, adminAuth, async (req, res) => { try { const { allocationId } = req.params; const adminId = req.user.id; const connection = await db.getDB().getConnection(); try { await connection.query('START TRANSACTION'); // 检查分配是否存在且状态为pending const [allocations] = await connection.execute( `SELECT oa.*, u_from.username as from_username, u_to.username as to_username FROM transfers oa JOIN users u_from ON oa.from_user_id = u_from.id JOIN users u_to ON oa.to_user_id = u_to.id WHERE oa.source_type = 'allocation' AND oa.id = ? AND oa.status = 'pending'`, [allocationId] ); if (allocations.length === 0) { await connection.query('ROLLBACK'); connection.release(); return res.status(404).json({ message: '分配不存在或状态不是待处理' }); } const allocation = allocations[0]; // 获取当前时间 const currentTime = new Date(); // 计算3小时后的截止时间 const deadline = new Date(); deadline.setHours(deadline.getHours() + 3); // 创建转账记录,直接设置为confirmed状态 const transferDescription = `匹配订单 ${allocation.matching_order_id} 第 ${allocation.cycle_number} 轮转账(管理员确认)`; const [transferResult] = await connection.execute( `INSERT INTO transfers (from_user_id, to_user_id, amount, transfer_type, status, description, deadline_at, confirmed_at, source_type) VALUES (?, ?, ?, "user_to_user", "confirmed", ?, ?, ?, 'allocation')`, [ allocation.from_user_id, allocation.to_user_id, allocation.amount, transferDescription, deadline, currentTime ] ); // 更新分配状态为已确认,并关联转账记录 await connection.execute( 'UPDATE transfers SET status = "confirmed", transfer_id = ?, confirmed_at = ?, updated_at = ? WHERE id = ?', [transferResult.insertId, currentTime, currentTime, allocationId] ); // 记录管理员操作日志 await connection.execute( 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "confirm_allocation", "allocation", ?, ?, ?)', [adminId, allocationId, `管理员确认分配:${allocation.from_username} -> ${allocation.to_username},金额:${allocation.amount}元`, currentTime] ); // 记录确认动作到匹配记录 await connection.execute( 'INSERT INTO matching_records (matching_order_id, user_id, action, amount, note) VALUES (?, ?, "confirm", ?, ?)', [ allocation.matching_order_id, adminId, allocation.amount, '管理员确认分配' ] ); await connection.query('COMMIT'); connection.release(); res.json({ success: true, message: '分配已确认' }); } catch (innerError) { await connection.query('ROLLBACK'); connection.release(); throw innerError; } } catch (error) { console.error('确认分配失败:', error); res.status(500).json({ message: error.message || '确认分配失败' }); } }); /** * @swagger * /api/matching-admin/cancel-allocation/{allocationId}: * post: * summary: 管理员取消分配 * tags: [MatchingAdmin] * security: * - bearerAuth: [] * parameters: * - in: path * name: allocationId * required: true * schema: * type: integer * description: 分配ID * responses: * 200: * description: 分配取消成功 * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * message: * type: string * 401: * description: 未授权 * 403: * description: 无管理员权限 * 404: * description: 分配不存在或状态不是待处理 * 500: * description: 服务器错误 */ router.post('/cancel-allocation/:allocationId', auth, adminAuth, async (req, res) => { try { const { allocationId } = req.params; const adminId = req.user.id; const connection = await db.getDB().getConnection(); try { await connection.query('START TRANSACTION'); // 检查分配是否存在且状态为pending const [allocations] = await connection.execute( `SELECT oa.*, u_from.username as from_username, u_to.username as to_username FROM transfers oa JOIN users u_from ON oa.from_user_id = u_from.id JOIN users u_to ON oa.to_user_id = u_to.id WHERE oa.source_type = 'allocation' AND oa.id = ? AND oa.status = 'pending'`, [allocationId] ); if (allocations.length === 0) { await connection.query('ROLLBACK'); connection.release(); return res.status(404).json({ message: '分配不存在或状态不是待处理' }); } const allocation = allocations[0]; // 获取当前时间 const currentTime = new Date(); // 更新分配状态为已取消 await connection.execute( 'UPDATE transfers SET status = "cancelled", updated_at = ? WHERE id = ?', [currentTime, allocationId] ); // 记录管理员操作日志 await connection.execute( 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "cancel_allocation", "allocation", ?, ?, ?)', [adminId, allocationId, `管理员取消分配:${allocation.from_username} -> ${allocation.to_username},金额:${allocation.amount}元`, currentTime] ); await connection.query('COMMIT'); connection.release(); res.json({ success: true, message: '分配已取消' }); } catch (innerError) { await connection.query('ROLLBACK'); connection.release(); throw innerError; } } catch (error) { console.error('取消分配失败:', error); res.status(500).json({ message: error.message || '取消分配失败' }); } }); module.exports = router;