feat: add VIP gating on cloud sync write endpoints + daily expiry reminder cron
- Add requireVip middleware to POST/PUT/DELETE /api/user-equity routes - Extract sendExpiryReminders() from route handler for reuse - Add node-cron job at 21:30 Asia/Shanghai daily for expiry reminders Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+49
-3
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
|
||||
const connectDB = require('./config/database');
|
||||
@@ -21,11 +22,23 @@ const settingsRoutes = require('./routes/settings');
|
||||
const subscribeRoutes = require('./routes/subscribe');
|
||||
const wechatMessageRoutes = require('./routes/wechatMessage');
|
||||
|
||||
const app = express();
|
||||
const cron = require('node-cron');
|
||||
|
||||
const app = express();
|
||||
connectDB();
|
||||
|
||||
app.use(helmet());
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'"],
|
||||
scriptSrcAttr: ["'unsafe-inline'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https:"],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
connectSrc: ["'self'", "https:"],
|
||||
},
|
||||
},
|
||||
}));
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
@@ -34,6 +47,9 @@ if (process.env.NODE_ENV === 'development') {
|
||||
app.use(morgan('dev'));
|
||||
}
|
||||
|
||||
app.use('/uploads', express.static(path.join(__dirname, '../public/uploads')));
|
||||
app.use('/admin', express.static(path.join(__dirname, '../public/admin')));
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
@@ -56,6 +72,25 @@ app.use('/api/subscribe', subscribeRoutes);
|
||||
|
||||
app.use('/wechat-message', wechatMessageRoutes);
|
||||
|
||||
const adminRoutes = require('./routes/admin');
|
||||
app.use('/api/admin', adminRoutes);
|
||||
|
||||
const { sendExpiryReminders } = require('./routes/subscribe');
|
||||
cron.schedule('30 21 * * *', async () => {
|
||||
console.log(`[${new Date().toISOString()}] ⏰ 到期提醒定时任务开始执行...`);
|
||||
try {
|
||||
const results = await sendExpiryReminders();
|
||||
console.log(`[${new Date().toISOString()}] ✅ 到期提醒执行完成: 共${results.total}条, 发送成功${results.sent}条, 失败${results.failed}条`);
|
||||
if (results.errors.length > 0) {
|
||||
console.error('失败详情:', JSON.stringify(results.errors.slice(0, 10)));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] ❌ 到期提醒执行失败:`, error.message);
|
||||
}
|
||||
}, {
|
||||
timezone: 'Asia/Shanghai'
|
||||
});
|
||||
|
||||
app.use(notFound);
|
||||
app.use(errorHandler);
|
||||
|
||||
@@ -64,7 +99,18 @@ const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`🚀 权益小助手后端服务运行在端口 ${PORT}`);
|
||||
console.log(`📊 环境: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log(`🌐 访问地址: https://api.dxz99wyr.cn`);
|
||||
console.log(`🌐 访问地址: https://api-miniapp.dxz99wyr.cn`);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('💥 未捕获异常 - 进程即将退出:', error.message);
|
||||
console.error('Stack:', error.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('💥 未处理的Promise拒绝:', reason);
|
||||
if (reason && reason.stack) console.error('Stack:', reason.stack);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
+62
-59
@@ -154,80 +154,83 @@ router.post('/expiry-save', auth, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/send-expiry-reminders', async (req, res, next) => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const results = { total: 0, sent: 0, failed: 0, errors: [] };
|
||||
async function sendExpiryReminders() {
|
||||
const now = new Date();
|
||||
const results = { total: 0, sent: 0, failed: 0, errors: [] };
|
||||
|
||||
const daysToCheck = [10, 3, 0];
|
||||
const daysToCheck = [10, 3, 0];
|
||||
|
||||
for (const daysBefore of daysToCheck) {
|
||||
const targetDate = new Date(now);
|
||||
targetDate.setDate(targetDate.getDate() + daysBefore);
|
||||
const targetDateStr = targetDate.toISOString().split('T')[0];
|
||||
for (const daysBefore of daysToCheck) {
|
||||
const targetDate = new Date(now);
|
||||
targetDate.setDate(targetDate.getDate() + daysBefore);
|
||||
const targetDateStr = targetDate.toISOString().split('T')[0];
|
||||
|
||||
const subscriptions = await UserSubscription.find({
|
||||
type: 'expiry-reminder',
|
||||
status: 'active',
|
||||
lastSentDay: { $ne: daysBefore }
|
||||
});
|
||||
const subscriptions = await UserSubscription.find({
|
||||
type: 'expiry-reminder',
|
||||
status: 'active',
|
||||
lastSentDay: { $ne: daysBefore }
|
||||
});
|
||||
|
||||
if (subscriptions.length === 0) continue;
|
||||
if (subscriptions.length === 0) continue;
|
||||
|
||||
for (const sub of subscriptions) {
|
||||
try {
|
||||
const equities = await UserEquity.find({
|
||||
owner: sub.userId,
|
||||
status: 'active',
|
||||
expireDate: targetDateStr
|
||||
});
|
||||
for (const sub of subscriptions) {
|
||||
try {
|
||||
const equities = await UserEquity.find({
|
||||
owner: sub.userId,
|
||||
status: 'active',
|
||||
expireDate: targetDateStr
|
||||
});
|
||||
|
||||
for (const equity of equities) {
|
||||
const dayLabel = daysBefore === 0 ? '今天' : `${daysBefore}天`;
|
||||
const result = await WechatSubscribeService.sendExpiryReminderMessage({
|
||||
openid: sub.openid,
|
||||
templateId: sub.templateId,
|
||||
thing1: equity.platform,
|
||||
thing2: `您的${equity.platform}${equity.type}将在${dayLabel}到期`,
|
||||
phrase3: daysBefore === 0 ? '已到期' : '即将到期'
|
||||
});
|
||||
|
||||
results.total++;
|
||||
|
||||
if (result.success) {
|
||||
results.sent++;
|
||||
sub.lastSentDay = daysBefore;
|
||||
await sub.save();
|
||||
} else {
|
||||
results.failed++;
|
||||
if (result.errcode === 43101 || result.errcode === 40037) {
|
||||
sub.status = 'expired';
|
||||
sub.expiredAt = new Date();
|
||||
await sub.save();
|
||||
}
|
||||
results.errors.push({
|
||||
openid: sub.openid,
|
||||
platform: equity.platform,
|
||||
error: result.errmsg
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (subError) {
|
||||
results.errors.push({
|
||||
for (const equity of equities) {
|
||||
const dayLabel = daysBefore === 0 ? '今天' : `${daysBefore}天`;
|
||||
const result = await WechatSubscribeService.sendExpiryReminderMessage({
|
||||
openid: sub.openid,
|
||||
error: subError.message
|
||||
templateId: sub.templateId,
|
||||
thing1: equity.platform,
|
||||
thing2: `您的${equity.platform}${equity.type}将在${dayLabel}到期`,
|
||||
phrase3: daysBefore === 0 ? '已到期' : '即将到期'
|
||||
});
|
||||
|
||||
results.total++;
|
||||
|
||||
if (result.success) {
|
||||
results.sent++;
|
||||
sub.lastSentDay = daysBefore;
|
||||
await sub.save();
|
||||
} else {
|
||||
results.failed++;
|
||||
if (result.errcode === 43101 || result.errcode === 40037) {
|
||||
sub.status = 'expired';
|
||||
sub.expiredAt = new Date();
|
||||
await sub.save();
|
||||
}
|
||||
results.errors.push({
|
||||
openid: sub.openid,
|
||||
platform: equity.platform,
|
||||
error: result.errmsg
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (subError) {
|
||||
results.errors.push({
|
||||
openid: sub.openid,
|
||||
error: subError.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
router.post('/send-expiry-reminders', async (req, res, next) => {
|
||||
try {
|
||||
const results = await sendExpiryReminders();
|
||||
res.json({ success: true, data: results });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports.sendExpiryReminders = sendExpiryReminders;
|
||||
@@ -3,6 +3,16 @@ const router = express.Router();
|
||||
const { auth } = require('../middleware/auth');
|
||||
const UserEquity = require('../models/UserEquity');
|
||||
|
||||
const requireVip = (req, res, next) => {
|
||||
if (!req.user.isVip) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: '非VIP用户无法使用云端同步功能,请升级会员'
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
router.get('/', auth, async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
@@ -114,7 +124,7 @@ router.get('/:id', auth, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', auth, async (req, res, next) => {
|
||||
router.post('/', auth, requireVip, async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
platform,
|
||||
@@ -171,7 +181,7 @@ router.post('/', auth, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/batch', auth, async (req, res, next) => {
|
||||
router.post('/batch', auth, requireVip, async (req, res, next) => {
|
||||
try {
|
||||
const { items } = req.body;
|
||||
|
||||
@@ -250,7 +260,7 @@ router.post('/batch', auth, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:id', auth, async (req, res, next) => {
|
||||
router.put('/:id', auth, requireVip, async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
platform,
|
||||
@@ -310,7 +320,7 @@ router.put('/:id', auth, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:id/benefits', auth, async (req, res, next) => {
|
||||
router.put('/:id/benefits', auth, requireVip, async (req, res, next) => {
|
||||
try {
|
||||
const { benefit, action } = req.body;
|
||||
|
||||
@@ -391,7 +401,7 @@ router.put('/:id/benefits', auth, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id', auth, async (req, res, next) => {
|
||||
router.delete('/:id', auth, requireVip, async (req, res, next) => {
|
||||
try {
|
||||
const equity = await UserEquity.findOneAndDelete({
|
||||
_id: req.params.id,
|
||||
|
||||
Reference in New Issue
Block a user