init: 小程序后台 — 到期提醒、定时任务、Docker部署配置
This commit is contained in:
+240
-99
@@ -1,99 +1,240 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { auth } = require('../middleware/auth');
|
||||
const User = require('../models/User');
|
||||
const { downloadAndSaveAvatar } = require('../services/avatarService');
|
||||
|
||||
router.get('/profile', auth, async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findById(req.user._id)
|
||||
.select('userId nickname avatarUrl status isVip vipExpireAt ocrCount ocrCountTotal platformLimit platformCount lastLoginAt');
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
userId: user.userId,
|
||||
nickname: user.nickname,
|
||||
avatarUrl: user.avatarUrl,
|
||||
status: user.status,
|
||||
isVip: user.isVip,
|
||||
vipExpireAt: user.vipExpireAt,
|
||||
ocrCount: user.ocrCount,
|
||||
ocrCountTotal: user.ocrCountTotal,
|
||||
platformLimit: user.platformLimit,
|
||||
platformCount: user.platformCount,
|
||||
lastLoginAt: user.lastLoginAt
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/profile', auth, async (req, res, next) => {
|
||||
try {
|
||||
const allowedUpdates = ['nickname', 'avatarUrl', 'phoneNumber', 'profile'];
|
||||
const updates = {};
|
||||
|
||||
Object.keys(req.body).forEach(key => {
|
||||
if (allowedUpdates.includes(key)) {
|
||||
updates[key] = req.body[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (updates.avatarUrl && updates.avatarUrl.startsWith('http')) {
|
||||
const savedAvatarUrl = await downloadAndSaveAvatar(updates.avatarUrl);
|
||||
if (savedAvatarUrl) {
|
||||
updates.avatarUrl = savedAvatarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.user._id,
|
||||
updates,
|
||||
{ new: true, runValidators: true }
|
||||
).select('-__v');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/stats', auth, async (req, res, next) => {
|
||||
try {
|
||||
const Equity = require('../models/Equity');
|
||||
const Trade = require('../models/Trade');
|
||||
|
||||
const [totalEquities, activeEquities, totalTrades, sellingTrades] = await Promise.all([
|
||||
Equity.countDocuments({ owner: req.user._id }),
|
||||
Equity.countDocuments({ owner: req.user._id, status: 'active' }),
|
||||
Trade.countDocuments({ $or: [{ seller: req.user._id }, { buyer: req.user._id }] }),
|
||||
Trade.countDocuments({ seller: req.user._id, status: 'pending' })
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalEquities,
|
||||
activeEquities,
|
||||
totalTrades,
|
||||
sellingTrades
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { auth } = require('../middleware/auth');
|
||||
const User = require('../models/User');
|
||||
const { downloadAndSaveAvatar } = require('../services/avatarService');
|
||||
const { compressAvatar } = require('../services/imageService');
|
||||
|
||||
const uploadDir = path.join(__dirname, '../../public/uploads/avatars');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname || '') || '.jpg';
|
||||
cb(null, 'avatar-' + uniqueSuffix + ext);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedTypes = /jpeg|jpg|png|gif|webp/;
|
||||
const extname = allowedTypes.test(path.extname(file.originalname || '').toLowerCase());
|
||||
const mimetype = allowedTypes.test(file.mimetype || '');
|
||||
if (extname || mimetype) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('只允许上传图片文件'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/profile', auth, async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findById(req.user._id);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
userId: user.userId || '',
|
||||
nickname: user.nickname || '',
|
||||
avatarUrl: user.avatarUrl || '',
|
||||
status: user.status || 'active',
|
||||
isVip: user.isVip || false,
|
||||
vipExpireAt: user.vipExpireAt || null,
|
||||
ocrCount: user.ocrCount || 10,
|
||||
ocrCountTotal: user.ocrCountTotal || 10,
|
||||
platformLimit: user.platformLimit || 15,
|
||||
platformCount: user.platformCount || 0,
|
||||
lastLoginAt: user.lastLoginAt || null
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/profile', auth, async (req, res, next) => {
|
||||
try {
|
||||
const allowedUpdates = ['nickname', 'avatarUrl', 'phoneNumber', 'profile'];
|
||||
const updates = {};
|
||||
|
||||
Object.keys(req.body).forEach(key => {
|
||||
if (allowedUpdates.includes(key)) {
|
||||
updates[key] = req.body[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (updates.avatarUrl && updates.avatarUrl.startsWith('http')) {
|
||||
const savedAvatarUrl = await downloadAndSaveAvatar(updates.avatarUrl);
|
||||
if (savedAvatarUrl) {
|
||||
updates.avatarUrl = savedAvatarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.user._id,
|
||||
updates,
|
||||
{ new: true, runValidators: true }
|
||||
).select('-__v');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/avatar', auth, upload.single('avatar'), async (req, res, next) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
console.error('[头像上传] 未收到文件,req.body keys:', Object.keys(req.body || {}));
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '没有上传文件'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[头像上传] 收到文件:', req.file.originalname, '大小:', req.file.size, '类型:', req.file.mimetype);
|
||||
console.log('[头像上传] 临时路径:', req.file.path);
|
||||
|
||||
const originalPath = req.file.path;
|
||||
try {
|
||||
await compressAvatar(originalPath);
|
||||
console.log('[头像上传] 压缩完成');
|
||||
} catch (compressError) {
|
||||
console.error('[头像上传] 压缩失败:', compressError.message);
|
||||
console.error('[头像上传] 压缩错误栈:', compressError.stack);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: '图片处理失败,请重试'
|
||||
});
|
||||
}
|
||||
|
||||
let finalFilename = req.file.filename;
|
||||
const ext = path.extname(finalFilename).toLowerCase();
|
||||
if (ext !== '.jpg' && ext !== '.jpeg') {
|
||||
const newFilename = finalFilename.replace(ext, '.jpg');
|
||||
const newPath = path.join(uploadDir, newFilename);
|
||||
fs.renameSync(originalPath, newPath);
|
||||
finalFilename = newFilename;
|
||||
console.log('[头像上传] 文件名从', req.file.filename, '改为', newFilename);
|
||||
}
|
||||
|
||||
const avatarUrl = `${process.env.SERVER_URL || 'https://api-miniapp.dxz99wyr.cn'}/uploads/avatars/${finalFilename}`;
|
||||
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.user._id,
|
||||
{ avatarUrl },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
avatarUrl: user.avatarUrl,
|
||||
url: avatarUrl
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/stats', auth, async (req, res, next) => {
|
||||
try {
|
||||
const Equity = require('../models/Equity');
|
||||
const Trade = require('../models/Trade');
|
||||
|
||||
const [totalEquities, activeEquities, totalTrades, sellingTrades] = await Promise.all([
|
||||
Equity.countDocuments({ owner: req.user._id }),
|
||||
Equity.countDocuments({ owner: req.user._id, status: 'active' }),
|
||||
Trade.countDocuments({ $or: [{ seller: req.user._id }, { buyer: req.user._id }] }),
|
||||
Trade.countDocuments({ seller: req.user._id, status: 'pending' })
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalEquities,
|
||||
activeEquities,
|
||||
totalTrades,
|
||||
sellingTrades
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/growth-stats', auth, async (req, res, next) => {
|
||||
try {
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
thirtyDaysAgo.setHours(0, 0, 0, 0);
|
||||
|
||||
const dailyCounts = await User.aggregate([
|
||||
{
|
||||
$match: {
|
||||
createdAt: { $gte: thirtyDaysAgo }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: '%Y-%m-%d', date: '$createdAt', timezone: 'Asia/Shanghai' }
|
||||
},
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{ $sort: { _id: 1 } }
|
||||
]);
|
||||
|
||||
const countMap = {};
|
||||
dailyCounts.forEach(item => {
|
||||
countMap[item._id] = item.count;
|
||||
});
|
||||
|
||||
const list = [];
|
||||
let cumulative = 0;
|
||||
|
||||
for (let i = 0; i <= 30; i++) {
|
||||
const d = new Date(thirtyDaysAgo);
|
||||
d.setDate(d.getDate() + i);
|
||||
const dateStr = d.toISOString().split('T')[0];
|
||||
const month = d.getMonth() + 1;
|
||||
const day = d.getDate();
|
||||
|
||||
cumulative += countMap[dateStr] || 0;
|
||||
list.push({
|
||||
date: `${month}月${day}日`,
|
||||
userCount: cumulative
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ success: true, data: { list } });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user