621 lines
20 KiB
JavaScript
621 lines
20 KiB
JavaScript
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; |