Files
jurong_circle_black/routes/matchingAdmin.js
2025-09-17 14:00:46 +08:00

621 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/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;