diff --git a/REPOS.md b/REPOS.md new file mode 100644 index 0000000..5e82784 --- /dev/null +++ b/REPOS.md @@ -0,0 +1,60 @@ +# 小程序后台 — 部署配置参考 + +> 摘录自 `D:\003_Project\004_Git\REPOS.md`,仅保留公共部分 + miniapp-api 相关内容。 + +## 服务器 + +| 项 | 值 | +|----|-----| +| IP | `8.136.137.59` | +| SSH 密钥 | `D:\003_Project\小程序连接.pem` | +| SSH 连接 | `ssh -i "D:\003_Project\小程序连接.pem" root@8.136.137.59` | + +## Gitea + +| 项 | 值 | +|----|-----| +| 网址 | `https://git.dxz99wyr.cn` | +| 用户名 | `Superuser` | +| 密码 | `Admin@123` | +| SSH Git 端口 | `2222` | +| SSH Git 地址格式 | `ssh://git@8.136.137.59:2222/Superuser/<仓库名>.git` | +| HTTP Git 地址格式 | `https://git.dxz99wyr.cn/Superuser/<仓库名>.git` | + +## miniapp-api(小程序后台) + +| 项 | 值 | +|----|-----| +| Git 地址 (SSH) | `ssh://git@8.136.137.59:2222/Superuser/miniapp-api.git` | +| Git 地址 (HTTPS) | `https://git.dxz99wyr.cn/Superuser/miniapp-api.git` | +| 本地路径 | `D:\003_Project\WeixinProject\QuanYiXiaoZhuShou\backend` | +| 服务器路径 | `/opt/ALiYunManager/services/miniapp-api` | +| Docker 服务名 | `miniapp-api` | +| 容器名 | `miniapp-api` | +| 部署方式 | `git pull` → `docker compose -f /opt/ALiYunManager/docker-compose.yml up -d --no-deps --force-recreate miniapp-api` | +| Webhook Secret | `miniapp-api-deploy-secret` | + +## 日常开发流程 + +```bash +git add . +git commit -m "feat: 描述你的改动" +git push +# ↑ push 后自动触发 Webhook → 服务器拉代码 → 重启容器,无需手动部署 +``` + +## 服务器常用命令 + +```bash +# 查看所有容器 +ssh -i "D:\003_Project\小程序连接.pem" root@8.136.137.59 "docker ps" + +# 查看部署日志 +ssh -i "D:\003_Project\小程序连接.pem" root@8.136.137.59 "tail -f /home/Git/logs/deploy.log" + +# 重启 miniapp-api +ssh -i "D:\003_Project\小程序连接.pem" root@8.136.137.59 "cd /opt/ALiYunManager && docker compose up -d --no-deps --force-recreate miniapp-api" + +# 重载 main-nginx +ssh -i "D:\003_Project\小程序连接.pem" root@8.136.137.59 "docker exec main-nginx nginx -t && docker exec main-nginx nginx -s reload" +``` diff --git a/src/app.js b/src/app.js index 17f4654..4a0e18f 100644 --- a/src/app.js +++ b/src/app.js @@ -20,6 +20,7 @@ const equityDetailRoutes = require('./routes/equityDetail'); const membershipRoutes = require('./routes/membership'); const settingsRoutes = require('./routes/settings'); const subscribeRoutes = require('./routes/subscribe'); +const uploadRoutes = require('./routes/upload'); const wechatMessageRoutes = require('./routes/wechatMessage'); const cron = require('node-cron'); @@ -69,6 +70,7 @@ app.use('/api/equity-detail', equityDetailRoutes); app.use('/api/membership', membershipRoutes); app.use('/api/settings', settingsRoutes); app.use('/api/subscribe', subscribeRoutes); +app.use('/api/upload', uploadRoutes); app.use('/wechat-message', wechatMessageRoutes); @@ -76,7 +78,7 @@ const adminRoutes = require('./routes/admin'); app.use('/api/admin', adminRoutes); const { sendExpiryReminders } = require('./routes/subscribe'); -cron.schedule('30 21 * * *', async () => { +cron.schedule('40 9 * * *', async () => { console.log(`[${new Date().toISOString()}] ⏰ 到期提醒定时任务开始执行...`); try { const results = await sendExpiryReminders(); diff --git a/src/models/UserEquity.js b/src/models/UserEquity.js index 51fb8a5..51f1734 100644 --- a/src/models/UserEquity.js +++ b/src/models/UserEquity.js @@ -120,8 +120,8 @@ const userEquitySchema = new mongoose.Schema({ default: 'active' }, note: { - type: String, - default: '' + text: { type: String, default: '' }, + images: [{ type: String }] }, hasUsedBenefit: { type: Boolean, diff --git a/src/routes/subscribe.js b/src/routes/subscribe.js index 1fa3981..a96bf3f 100644 --- a/src/routes/subscribe.js +++ b/src/routes/subscribe.js @@ -177,17 +177,49 @@ async function sendExpiryReminders() { try { const equities = await UserEquity.find({ owner: sub.userId, - status: 'active', - expireDate: targetDateStr + status: 'active' }); for (const equity of equities) { const dayLabel = daysBefore === 0 ? '今天' : `${daysBefore}天`; + let shouldSend = false; + let reminderTarget = null; + + if (equity.expireDate === targetDateStr) { + shouldSend = true; + reminderTarget = { + name: `${equity.platform}${equity.type}`, + type: 'platform' + }; + } + + if (!shouldSend && equity.benefits && equity.benefits.length > 0) { + for (const benefit of equity.benefits) { + if (benefit.expireDate === targetDateStr) { + shouldSend = true; + reminderTarget = { + name: benefit.name, + type: 'benefit' + }; + break; + } + } + } + + if (!shouldSend) continue; + + let thing2; + if (reminderTarget.type === 'platform') { + thing2 = `您的${reminderTarget.name}将在${dayLabel}到期`; + } else { + thing2 = `您的${equity.platform}权益「${reminderTarget.name}」将在${dayLabel}到期`; + } + const result = await WechatSubscribeService.sendExpiryReminderMessage({ openid: sub.openid, templateId: sub.templateId, thing1: equity.platform, - thing2: `您的${equity.platform}${equity.type}将在${dayLabel}到期`, + thing2: thing2, phrase3: daysBefore === 0 ? '已到期' : '即将到期' }); diff --git a/src/routes/upload.js b/src/routes/upload.js new file mode 100644 index 0000000..b626c89 --- /dev/null +++ b/src/routes/upload.js @@ -0,0 +1,63 @@ +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 uploadDir = path.join(__dirname, '../../public/uploads/equity-notes'); +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, 'note-' + uniqueSuffix + ext); + } +}); + +const upload = multer({ + storage, + limits: { fileSize: 10 * 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.post('/image', auth, upload.single('file'), async (req, res, next) => { + try { + if (!req.file) { + return res.status(400).json({ + success: false, + error: '请选择要上传的图片' + }); + } + + const filename = req.file.filename; + const baseUrl = process.env.SERVER_URL || 'https://api-miniapp.dxz99wyr.cn'; + const url = `${baseUrl}/uploads/equity-notes/${filename}`; + + res.json({ + success: true, + data: { + url + } + }); + } catch (error) { + next(error); + } +}); + +module.exports = router; diff --git a/src/routes/userEquity.js b/src/routes/userEquity.js index 3ebfba8..ea400a8 100644 --- a/src/routes/userEquity.js +++ b/src/routes/userEquity.js @@ -158,7 +158,10 @@ router.post('/', auth, requireVip, async (req, res, next) => { benefits: benefits || [], owner: req.user._id, status: status || 'active', - note: note || '', + note: { + text: (note && note.text) || '', + images: (note && note.images) || [] + }, syncedAt: new Date().toISOString() }; @@ -227,7 +230,10 @@ router.post('/batch', auth, requireVip, async (req, res, next) => { benefits: benefits || [], owner: req.user._id, status: status || 'active', - note: note || '', + note: { + text: (note && note.text) || '', + images: (note && note.images) || [] + }, syncedAt: new Date().toISOString() }; @@ -287,7 +293,10 @@ router.put('/:id', auth, requireVip, async (req, res, next) => { if (price !== undefined) updates.price = parseFloat(price) || 0; if (benefits !== undefined) updates.benefits = benefits; if (status !== undefined) updates.status = status; - if (note !== undefined) updates.note = note; + if (note !== undefined) updates.note = { + text: (note && note.text) || '', + images: (note && note.images) || [] + }; if (platformType !== undefined) updates.platformType = platformType; if (brandIcon !== undefined) updates.brandIcon = brandIcon; if (brandIconImage !== undefined) updates.brandIconImage = brandIconImage;