Files
jurong_circle_black/routes/points.js
2025-08-28 09:14:56 +08:00

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;