697 lines
20 KiB
JavaScript
697 lines
20 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const { getDB } = require('../database');
|
|
const { auth, adminAuth } = require('../middleware/auth');
|
|
|
|
/**
|
|
* @swagger
|
|
* tags:
|
|
* name: Points
|
|
* description: 积分管理相关接口
|
|
*/
|
|
|
|
/**
|
|
* @swagger
|
|
* components:
|
|
* schemas:
|
|
* PointsHistory:
|
|
* type: object
|
|
* properties:
|
|
* id:
|
|
* type: integer
|
|
* description: 积分历史记录ID
|
|
* points_change:
|
|
* type: integer
|
|
* description: 积分变动数量
|
|
* type:
|
|
* type: string
|
|
* description: 积分变动类型(earn-获得, spend-消费, admin_adjust-管理员调整)
|
|
* description:
|
|
* type: string
|
|
* description: 积分变动描述
|
|
* created_at:
|
|
* type: string
|
|
* format: date-time
|
|
* description: 创建时间
|
|
*/
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/points/balance:
|
|
* get:
|
|
* summary: 获取用户当前积分余额
|
|
* tags: [Points]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* responses:
|
|
* 200:
|
|
* description: 成功获取积分余额
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* data:
|
|
* type: object
|
|
* properties:
|
|
* points:
|
|
* type: integer
|
|
* description: 用户当前积分
|
|
* 401:
|
|
* description: 未授权,需要登录
|
|
* 404:
|
|
* description: 用户不存在
|
|
* 500:
|
|
* description: 服务器错误
|
|
*/
|
|
router.get('/balance', auth, async (req, res) => {
|
|
try {
|
|
const userId = req.user.id;
|
|
|
|
const [users] = await getDB().execute(
|
|
'SELECT points FROM users WHERE id = ?',
|
|
[userId]
|
|
);
|
|
|
|
if (users.length === 0) {
|
|
return res.status(404).json({ success: false, message: '用户不存在' });
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
points: users[0].points
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('获取积分余额失败:', error);
|
|
res.status(500).json({ success: false, message: '获取积分余额失败' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/points/history:
|
|
* get:
|
|
* summary: 获取用户积分历史记录
|
|
* tags: [Points]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: query
|
|
* name: page
|
|
* schema:
|
|
* type: integer
|
|
* default: 1
|
|
* description: 页码
|
|
* - in: query
|
|
* name: limit
|
|
* schema:
|
|
* type: integer
|
|
* default: 10
|
|
* description: 每页记录数
|
|
* - in: query
|
|
* name: type
|
|
* schema:
|
|
* type: string
|
|
* enum: [earn, spend, admin_adjust]
|
|
* description: 积分变动类型
|
|
* - in: query
|
|
* name: username
|
|
* schema:
|
|
* type: string
|
|
* description: 用户名(仅管理员可用)
|
|
* - in: query
|
|
* name: change
|
|
* schema:
|
|
* type: string
|
|
* enum: [positive, negative]
|
|
* description: 积分变动方向(仅管理员可用)
|
|
* - in: query
|
|
* name: startDate
|
|
* schema:
|
|
* type: string
|
|
* format: date
|
|
* description: 开始日期(仅管理员可用)
|
|
* - in: query
|
|
* name: endDate
|
|
* schema:
|
|
* type: string
|
|
* format: date
|
|
* description: 结束日期(仅管理员可用)
|
|
* responses:
|
|
* 200:
|
|
* description: 成功获取积分历史
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* data:
|
|
* type: object
|
|
* properties:
|
|
* records:
|
|
* type: array
|
|
* items:
|
|
* $ref: '#/components/schemas/PointsHistory'
|
|
* pagination:
|
|
* type: object
|
|
* properties:
|
|
* page:
|
|
* type: integer
|
|
* limit:
|
|
* type: integer
|
|
* total:
|
|
* type: integer
|
|
* totalPages:
|
|
* type: integer
|
|
* 401:
|
|
* description: 未授权,需要登录
|
|
* 500:
|
|
* description: 服务器错误
|
|
*/
|
|
router.get('/history', auth, async (req, res) => {
|
|
try {
|
|
const { page = 1, limit = 10, type, username, change, startDate, endDate } = req.query;
|
|
|
|
// 确保参数为有效数字
|
|
const pageNum = parseInt(page) || 1;
|
|
const limitNum = parseInt(limit) || 10;
|
|
const offset = (pageNum - 1) * limitNum;
|
|
|
|
let whereClause = '';
|
|
let queryParams = [];
|
|
|
|
// 如果是管理员,可以查看所有用户的积分历史
|
|
if (req.user.role === 'admin') {
|
|
whereClause = 'WHERE 1=1';
|
|
|
|
// 按用户名筛选
|
|
if (username) {
|
|
whereClause += ' AND u.username LIKE ?';
|
|
queryParams.push(`%${username}%`);
|
|
}
|
|
|
|
// 按类型筛选
|
|
if (type) {
|
|
whereClause += ' AND ph.type = ?';
|
|
queryParams.push(type);
|
|
}
|
|
|
|
// 按积分变化筛选
|
|
if (change === 'positive') {
|
|
whereClause += ' AND ph.amount > 0';
|
|
} else if (change === 'negative') {
|
|
whereClause += ' AND ph.amount < 0';
|
|
}
|
|
|
|
// 按时间范围筛选
|
|
if (startDate) {
|
|
whereClause += ' AND DATE(ph.created_at) >= ?';
|
|
queryParams.push(startDate);
|
|
}
|
|
if (endDate) {
|
|
whereClause += ' AND DATE(ph.created_at) <= ?';
|
|
queryParams.push(endDate);
|
|
}
|
|
} else {
|
|
// 普通用户只能查看自己的积分历史
|
|
whereClause = 'WHERE ph.user_id = ?';
|
|
queryParams.push(req.user.id);
|
|
|
|
if (type && ['earn', 'spend'].includes(type)) {
|
|
whereClause += ' AND ph.type = ?';
|
|
queryParams.push(type);
|
|
}
|
|
}
|
|
|
|
// 获取总数
|
|
const countQuery = req.user.role === 'admin'
|
|
? `SELECT COUNT(*) as total FROM points_history ph JOIN users u ON ph.user_id = u.id ${whereClause}`
|
|
: `SELECT COUNT(*) as total FROM points_history ph ${whereClause}`;
|
|
|
|
const [countResult] = await getDB().execute(countQuery, queryParams);
|
|
|
|
// 获取历史记录
|
|
const historyQuery = req.user.role === 'admin'
|
|
? `SELECT ph.id, ph.amount as points, ph.type, ph.description, ph.created_at,
|
|
u.username,
|
|
(SELECT points FROM users WHERE id = ph.user_id) as balance_after
|
|
FROM points_history ph
|
|
JOIN users u ON ph.user_id = u.id
|
|
${whereClause}
|
|
ORDER BY ph.created_at DESC
|
|
LIMIT ${limitNum} OFFSET ${offset}`
|
|
: `SELECT id, amount as points_change, type, description, created_at
|
|
FROM points_history ph
|
|
${whereClause}
|
|
ORDER BY created_at DESC
|
|
LIMIT ${limitNum} OFFSET ${offset}`;
|
|
|
|
const [records] = await getDB().execute(historyQuery, queryParams);
|
|
|
|
const responseData = req.user.role === 'admin'
|
|
? {
|
|
history: records,
|
|
total: countResult[0].total
|
|
}
|
|
: {
|
|
records,
|
|
pagination: {
|
|
page: pageNum,
|
|
limit: limitNum,
|
|
total: countResult[0].total,
|
|
totalPages: Math.ceil(countResult[0].total / limitNum)
|
|
}
|
|
};
|
|
|
|
res.json({
|
|
success: true,
|
|
data: responseData
|
|
});
|
|
} catch (error) {
|
|
console.error('获取积分历史失败:', error);
|
|
res.status(500).json({ success: false, message: '获取积分历史失败' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/points/adjust:
|
|
* post:
|
|
* summary: 管理员调整用户积分
|
|
* tags: [Points]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - userId
|
|
* - points
|
|
* - reason
|
|
* properties:
|
|
* userId:
|
|
* type: integer
|
|
* description: 用户ID
|
|
* points:
|
|
* type: integer
|
|
* description: 调整的积分数量(正数为增加,负数为减少)
|
|
* reason:
|
|
* type: string
|
|
* description: 调整原因
|
|
* responses:
|
|
* 200:
|
|
* description: 积分调整成功
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* message:
|
|
* type: string
|
|
* example: 积分调整成功
|
|
* data:
|
|
* type: object
|
|
* properties:
|
|
* userId:
|
|
* type: integer
|
|
* pointsChanged:
|
|
* type: integer
|
|
* newBalance:
|
|
* type: integer
|
|
* 400:
|
|
* description: 参数错误或积分不足
|
|
* 401:
|
|
* description: 未授权,需要管理员权限
|
|
* 404:
|
|
* description: 用户不存在
|
|
* 500:
|
|
* description: 服务器错误
|
|
*/
|
|
router.post('/adjust', auth, adminAuth, async (req, res) => {
|
|
const connection = await getDB().getConnection();
|
|
|
|
try {
|
|
await connection.beginTransaction();
|
|
|
|
const { userId, points, reason } = req.body;
|
|
|
|
if (!userId || points === undefined || points === null || !reason) {
|
|
await connection.rollback();
|
|
return res.status(400).json({ success: false, message: '请提供有效的用户ID、积分数量和调整原因' });
|
|
}
|
|
|
|
// 检查用户是否存在
|
|
const [users] = await connection.execute(
|
|
'SELECT id, username, points FROM users WHERE id = ?',
|
|
[userId]
|
|
);
|
|
|
|
if (users.length === 0) {
|
|
await connection.rollback();
|
|
return res.status(404).json({ success: false, message: '用户不存在' });
|
|
}
|
|
|
|
const currentPoints = users[0].points;
|
|
const newPoints = currentPoints + points;
|
|
|
|
// 检查积分是否会变为负数
|
|
if (newPoints < 0) {
|
|
await connection.rollback();
|
|
return res.status(400).json({ success: false, message: '用户积分不足,无法扣除' });
|
|
}
|
|
|
|
// 更新用户积分
|
|
await connection.execute(
|
|
'UPDATE users SET points = ? WHERE id = ?',
|
|
[newPoints, userId]
|
|
);
|
|
|
|
// 记录积分历史
|
|
await connection.execute(
|
|
`INSERT INTO points_history (user_id, amount, type, description, created_at)
|
|
VALUES (?, ?, 'admin_adjust', ?, NOW())`,
|
|
[userId, points, reason]
|
|
);
|
|
|
|
await connection.commit();
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '积分调整成功',
|
|
data: {
|
|
userId: userId,
|
|
pointsChanged: points,
|
|
newBalance: newPoints
|
|
}
|
|
});
|
|
} catch (error) {
|
|
await connection.rollback();
|
|
console.error('积分调整失败:', error);
|
|
res.status(500).json({ success: false, message: '积分调整失败' });
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/points/recharge:
|
|
* post:
|
|
* summary: 管理员给用户充值积分
|
|
* tags: [Points]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - user_id
|
|
* - points
|
|
* properties:
|
|
* user_id:
|
|
* type: integer
|
|
* description: 用户ID
|
|
* points:
|
|
* type: integer
|
|
* description: 充值的积分数量(必须为正数)
|
|
* description:
|
|
* type: string
|
|
* description: 充值描述
|
|
* default: 管理员充值
|
|
* responses:
|
|
* 200:
|
|
* description: 积分充值成功
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* message:
|
|
* type: string
|
|
* example: 积分充值成功
|
|
* data:
|
|
* type: object
|
|
* properties:
|
|
* userId:
|
|
* type: integer
|
|
* pointsAdded:
|
|
* type: integer
|
|
* 400:
|
|
* description: 参数错误
|
|
* 401:
|
|
* description: 未授权,需要管理员权限
|
|
* 404:
|
|
* description: 用户不存在
|
|
* 500:
|
|
* description: 服务器错误
|
|
*/
|
|
router.post('/recharge', auth, adminAuth, async (req, res) => {
|
|
const connection = await getDB().getConnection();
|
|
|
|
try {
|
|
await connection.beginTransaction();
|
|
|
|
const { user_id, points, description = '管理员充值' } = req.body;
|
|
|
|
if (!user_id || !points || points <= 0) {
|
|
await connection.rollback();
|
|
return res.status(400).json({ success: false, message: '请提供有效的用户ID和积分数量' });
|
|
}
|
|
|
|
// 检查用户是否存在
|
|
const [users] = await connection.execute(
|
|
'SELECT id, username FROM users WHERE id = ?',
|
|
[user_id]
|
|
);
|
|
|
|
if (users.length === 0) {
|
|
await connection.rollback();
|
|
return res.status(404).json({ success: false, message: '用户不存在' });
|
|
}
|
|
|
|
// 增加用户积分
|
|
await connection.execute(
|
|
'UPDATE users SET points = points + ? WHERE id = ?',
|
|
[points, user_id]
|
|
);
|
|
|
|
// 记录积分历史
|
|
await connection.execute(
|
|
`INSERT INTO points_history (user_id, amount, type, description, created_at)
|
|
VALUES (?, ?, 'earn', ?, NOW())`,
|
|
[user_id, points, description]
|
|
);
|
|
|
|
await connection.commit();
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '积分充值成功',
|
|
data: {
|
|
userId: user_id,
|
|
pointsAdded: points
|
|
}
|
|
});
|
|
} catch (error) {
|
|
await connection.rollback();
|
|
console.error('积分充值失败:', error);
|
|
res.status(500).json({ success: false, message: '积分充值失败' });
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/points/leaderboard:
|
|
* get:
|
|
* summary: 获取积分排行榜
|
|
* tags: [Points]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: query
|
|
* name: limit
|
|
* schema:
|
|
* type: integer
|
|
* default: 10
|
|
* description: 返回的排行榜数量
|
|
* responses:
|
|
* 200:
|
|
* description: 成功获取积分排行榜
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* data:
|
|
* type: object
|
|
* properties:
|
|
* leaderboard:
|
|
* type: array
|
|
* items:
|
|
* type: object
|
|
* properties:
|
|
* rank:
|
|
* type: integer
|
|
* userId:
|
|
* type: integer
|
|
* username:
|
|
* type: string
|
|
* points:
|
|
* type: integer
|
|
* 401:
|
|
* description: 未授权,需要登录
|
|
* 500:
|
|
* description: 服务器错误
|
|
*/
|
|
router.get('/leaderboard', auth, async (req, res) => {
|
|
try {
|
|
const { limit = 10 } = req.query;
|
|
|
|
const [users] = await getDB().execute(
|
|
`SELECT id, username, points
|
|
FROM users
|
|
WHERE points > 0
|
|
ORDER BY points DESC
|
|
LIMIT ?`,
|
|
[parseInt(limit)]
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
leaderboard: users.map((user, index) => ({
|
|
rank: index + 1,
|
|
userId: user.id,
|
|
username: user.username,
|
|
points: user.points
|
|
}))
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('获取积分排行榜失败:', error);
|
|
res.status(500).json({ success: false, message: '获取积分排行榜失败' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/points/stats:
|
|
* get:
|
|
* summary: 获取积分统计信息(管理员权限)
|
|
* tags: [Points]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* responses:
|
|
* 200:
|
|
* description: 成功获取积分统计信息
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* data:
|
|
* type: object
|
|
* properties:
|
|
* stats:
|
|
* type: object
|
|
* properties:
|
|
* totalPoints:
|
|
* type: integer
|
|
* description: 系统中总积分数量
|
|
* totalEarned:
|
|
* type: integer
|
|
* description: 总积分发放量
|
|
* totalSpent:
|
|
* type: integer
|
|
* description: 总积分消费量
|
|
* activeUsers:
|
|
* type: integer
|
|
* description: 活跃用户数
|
|
* 401:
|
|
* description: 未授权,需要管理员权限
|
|
* 500:
|
|
* description: 服务器错误
|
|
*/
|
|
router.get('/stats', auth, adminAuth, async (req, res) => {
|
|
try {
|
|
// 总积分发放量
|
|
const [totalEarned] = await getDB().execute(
|
|
'SELECT SUM(amount) as total FROM points_history WHERE type = "earn"'
|
|
);
|
|
|
|
// 总积分消费量
|
|
const [totalConsumed] = await getDB().execute(
|
|
'SELECT SUM(ABS(amount)) as total FROM points_history WHERE type = "spend"'
|
|
);
|
|
|
|
// 本月积分发放
|
|
const [monthEarned] = await getDB().execute(
|
|
'SELECT SUM(amount) as total FROM points_history WHERE type = "earn" AND YEAR(created_at) = YEAR(NOW()) AND MONTH(created_at) = MONTH(NOW())'
|
|
);
|
|
|
|
// 上月积分发放(用于计算增长率)
|
|
const [lastMonthEarned] = await getDB().execute(
|
|
'SELECT SUM(amount) as total FROM points_history WHERE type = "earn" AND YEAR(created_at) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND MONTH(created_at) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))'
|
|
);
|
|
|
|
// 计算月增长率
|
|
const lastMonthTotal = lastMonthEarned[0].total || 0;
|
|
const currentMonthTotal = monthEarned[0].total || 0;
|
|
let monthGrowthRate = 0;
|
|
if (lastMonthTotal > 0) {
|
|
monthGrowthRate = ((currentMonthTotal - lastMonthTotal) / lastMonthTotal * 100).toFixed(1);
|
|
}
|
|
|
|
// 活跃用户数(有积分记录的用户)
|
|
const [activeUsers] = await getDB().execute(
|
|
'SELECT COUNT(DISTINCT user_id) as count FROM points_history'
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
stats: {
|
|
totalPoints: totalEarned[0].total || 0,
|
|
totalEarned: totalEarned[0].total || 0,
|
|
totalSpent: totalConsumed[0].total || 0,
|
|
activeUsers: activeUsers[0].count
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('获取积分统计失败:', error);
|
|
res.status(500).json({ success: false, message: '获取积分统计失败' });
|
|
}
|
|
});
|
|
|
|
module.exports = router; |