Compare commits
4 Commits
e73149f91d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ca73922d8 | |||
| 344b25a2d8 | |||
| 608ee017d4 | |||
| 1117bd0bd7 |
+23
-5
@@ -52,7 +52,7 @@ chmod +x deploy/setup.sh
|
||||
|
||||
在 Git 仓库设置中添加 Webhook:
|
||||
|
||||
- **Payload URL**: `http://your-server-ip:9001/webhook`
|
||||
- **Payload URL**: `https://miniapp-api-test-webhook.dxz99wyr.cn/webhook`
|
||||
- **Content type**: `application/json`
|
||||
- **Secret**: 你设置的 `WEBHOOK_SECRET`
|
||||
- **触发事件**: Push events (main 分支)
|
||||
@@ -60,23 +60,41 @@ chmod +x deploy/setup.sh
|
||||
### 5. 开放防火墙端口
|
||||
|
||||
```bash
|
||||
# 开放 9001 端口(webhook)和 3001 端口(API)
|
||||
ufw allow 9001/tcp
|
||||
ufw allow 3001/tcp
|
||||
# 仅需开放 80/443 端口,所有服务通过 Nginx 反向代理
|
||||
ufw allow 80/tcp
|
||||
ufw allow 443/tcp
|
||||
```
|
||||
|
||||
### 6. 配置 Nginx 反向代理 (推荐)
|
||||
|
||||
```nginx
|
||||
# 复制到服务器的 Nginx 配置目录
|
||||
# cp deploy/nginx-test.conf /opt/ALiYunManager/nginx/conf.d/miniapp-api-test.conf
|
||||
# docker exec main-nginx nginx -t && docker exec main-nginx nginx -s reload
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name miniapp-api-test.dxz99wyr.cn;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3001;
|
||||
proxy_pass http://miniapp-api_test:3001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name miniapp-api-test-webhook.dxz99wyr.cn;
|
||||
|
||||
location /webhook {
|
||||
proxy_pass http://127.0.0.1:19001/webhook;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name miniapp-api-test.dxz99wyr.cn;
|
||||
|
||||
location / {
|
||||
proxy_pass http://miniapp-api_test:3001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name miniapp-api-test-webhook.dxz99wyr.cn;
|
||||
|
||||
location /webhook {
|
||||
proxy_pass http://127.0.0.1:19001/webhook;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -44,7 +44,7 @@ echo "=========================================="
|
||||
echo " 配置完成!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Webhook 接收地址: http://$(curl -s ifconfig.me):9001/webhook"
|
||||
echo "Webhook 接收地址: https://miniapp-api-test-webhook.dxz99wyr.cn/webhook"
|
||||
echo "API 测试地址: https://miniapp-api-test.dxz99wyr.cn"
|
||||
echo ""
|
||||
echo "查看 webhook 日志: journalctl -u miniapp-api_test-webhook -f"
|
||||
|
||||
@@ -3,7 +3,7 @@ const { exec } = require('child_process');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const PORT = 9001;
|
||||
const PORT = 19001;
|
||||
const DEPLOY_DIR = '/opt/miniapp-api_test';
|
||||
const COMPOSE_FILE = 'docker-compose.test.yml';
|
||||
|
||||
@@ -98,8 +98,9 @@ const server = http.createServer(async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
server.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(`Webhook 服务器已启动, 监听端口 ${PORT}`);
|
||||
console.log(`部署目录: ${DEPLOY_DIR}`);
|
||||
console.log(`接收地址: http://your-server-ip:${PORT}/webhook`);
|
||||
console.log(`接收地址: http://127.0.0.1:${PORT}/webhook`);
|
||||
console.log(`Nginx 代理地址: http://your-server-ip/webhook`);
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
container_name: miniapp-api_test
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3001:3001"
|
||||
expose:
|
||||
- "3001"
|
||||
environment:
|
||||
- PORT=3001
|
||||
- NODE_ENV=production
|
||||
@@ -38,8 +38,8 @@ services:
|
||||
image: mongo:7.0
|
||||
container_name: miniapp-api_test-mongo
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "27019:27017"
|
||||
expose:
|
||||
- "27017"
|
||||
volumes:
|
||||
- mongo_data_test:/data/db
|
||||
environment:
|
||||
|
||||
@@ -67,6 +67,9 @@ tr:hover td { background: #f8f9fa; }
|
||||
.badge-vip { background: #e8f0fe; color: #1967d2; }
|
||||
.btn-detail { padding: 4px 12px; background: var(--primary); color: #fff; border: none; border-radius: 4px; font-size: 12px; cursor: pointer; }
|
||||
.btn-detail:hover { background: var(--primary-hover); }
|
||||
.btn-send-msg { padding: 8px 16px; background: var(--primary); color: #fff; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; white-space: nowrap; }
|
||||
.btn-send-msg:hover { background: var(--primary-hover); }
|
||||
.btn-send-msg:disabled { opacity: .6; cursor: not-allowed; }
|
||||
|
||||
/* Pagination */
|
||||
.pagination { padding: 16px 20px; display: flex; justify-content: center; align-items: center; gap: 12px; }
|
||||
@@ -355,6 +358,7 @@ async function openModal(id) {
|
||||
try {
|
||||
const d = await api('/api/admin/users/' + id);
|
||||
S.editingUser = d.data;
|
||||
S.subscriptions = d.subscriptions || [];
|
||||
renderModal(d.data);
|
||||
} catch (e) {
|
||||
showToast('加载用户详情失败', false);
|
||||
@@ -404,7 +408,81 @@ function renderModal(u) {
|
||||
<div class="field-row">
|
||||
<div class="field"><label>平台限额</label><input type="number" id="edit-platformLimit" value="${u.platformLimit || 0}" min="0"></div>
|
||||
<div class="field"><label>当前平台数</label><input type="number" id="edit-platformCount" value="${u.platformCount || 0}" min="0"></div>
|
||||
</div>
|
||||
<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">
|
||||
<div class="field"><label>订阅通知状态(只读)</label><div id="subscribeStatus" class="ro">加载中...</div></div>
|
||||
<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">
|
||||
<div class="field"><label>发送测试消息</label>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<input id="test-tplId" value="DaPPZVFsLsHzj6TJAFK5rUp8B4DW-9JOtJF00v-uLMQ" style="flex:1;min-width:180px" placeholder="模板ID">
|
||||
<input id="test-str1" value="v2.0" style="width:100px" placeholder="character_string1">
|
||||
<input id="test-t3" value="版本已更新" style="width:100px" placeholder="thing3">
|
||||
<input id="test-t6" value="请查看更新内容" style="width:120px" placeholder="thing6">
|
||||
<button class="btn-send-msg" onclick="sendTestMessage()">发送测试</button>
|
||||
</div>
|
||||
<div id="testResult" style="margin-top:8px"></div>
|
||||
</div>`;
|
||||
if (S.subscriptions) {
|
||||
renderSubscriptions(S.subscriptions);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSubscriptions(subs) {
|
||||
const el = document.getElementById('subscribeStatus');
|
||||
if (!subs || subs.length === 0) {
|
||||
el.innerHTML = '<span style="color:var(--text-secondary)">该用户未订阅任何通知</span>';
|
||||
return;
|
||||
}
|
||||
const typeMap = {};
|
||||
subs.forEach(s => {
|
||||
if (!typeMap[s.type]) typeMap[s.type] = [];
|
||||
typeMap[s.type].push(s);
|
||||
});
|
||||
let html = '<div style="font-size:13px">';
|
||||
for (const [type, items] of Object.entries(typeMap)) {
|
||||
const latest = items[0];
|
||||
const badgeClass = latest.status === 'active' ? 'badge-active' : (latest.status === 'used' ? 'badge-inactive' : 'badge-banned');
|
||||
const label = latest.status === 'active' ? '已授权' : (latest.status === 'used' ? '已使用' : latest.status);
|
||||
const typeName = type === 'version-update' ? '版本更新通知' : (type === 'expiry-reminder' ? '到期提醒通知' : type);
|
||||
html += `<div style="margin-bottom:8px">
|
||||
<span class="badge ${badgeClass}">${label}</span>
|
||||
<span style="margin-left:8px">${typeName}</span>
|
||||
<span style="margin-left:8px;color:var(--text-secondary)">模板: ${latest.templateId.substring(0,8)}...</span>
|
||||
<span style="margin-left:8px;color:var(--text-secondary)">订阅: ${fmtDate(latest.subscribedAt)}</span>
|
||||
</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
async function sendTestMessage() {
|
||||
if (!S.editingUser) return;
|
||||
const btn = document.querySelector('.btn-send-msg');
|
||||
const resultEl = document.getElementById('testResult');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '发送中...';
|
||||
resultEl.innerHTML = '';
|
||||
try {
|
||||
const res = await api('/api/admin/users/' + S.editingUser._id + '/test-message', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
templateId: document.getElementById('test-tplId').value.trim(),
|
||||
character_string1: document.getElementById('test-str1').value.trim(),
|
||||
thing3: document.getElementById('test-t3').value.trim(),
|
||||
thing6: document.getElementById('test-t6').value.trim()
|
||||
})
|
||||
});
|
||||
if (res.success) {
|
||||
resultEl.innerHTML = `<span style="color:#137333">✅ 发送成功 (msgid: ${res.data.messageId})</span>`;
|
||||
} else {
|
||||
resultEl.innerHTML = `<span style="color:var(--danger)">❌ 发送失败: errcode=${res.data.errcode} ${res.data.errmsg || ''}</span>`;
|
||||
}
|
||||
} catch (e) {
|
||||
resultEl.innerHTML = `<span style="color:var(--danger)">❌ 异常: ${e.message}</span>`;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '发送测试';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleVipExpire() {
|
||||
|
||||
+6
-1
@@ -41,7 +41,12 @@ app.use(helmet({
|
||||
},
|
||||
},
|
||||
}));
|
||||
app.use(cors());
|
||||
app.use(cors({
|
||||
origin: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'Accept', 'Origin', 'X-Requested-With'],
|
||||
credentials: false
|
||||
}));
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
|
||||
+39
-1
@@ -1,6 +1,8 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const User = require('../models/User');
|
||||
const UserSubscription = require('../models/UserSubscription');
|
||||
const WechatSubscribeService = require('../services/wechatSubscribeService');
|
||||
const { adminAuth } = require('../middleware/adminAuth');
|
||||
|
||||
router.post('/login', (req, res) => {
|
||||
@@ -115,7 +117,43 @@ router.get('/users/:id', adminAuth, async (req, res, next) => {
|
||||
return res.status(404).json({ success: false, error: '用户不存在' });
|
||||
}
|
||||
|
||||
res.json({ success: true, data: user });
|
||||
if (!user.openid) {
|
||||
return res.json({ success: true, data: user, subscriptions: [] });
|
||||
}
|
||||
|
||||
const subscriptions = await UserSubscription.find({ openid: user.openid })
|
||||
.sort({ subscribedAt: -1 })
|
||||
.lean();
|
||||
|
||||
res.json({ success: true, data: user, subscriptions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/users/:id/test-message', adminAuth, async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findById(req.params.id).select('-__v').lean();
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ success: false, error: '用户不存在' });
|
||||
}
|
||||
|
||||
if (!user.openid) {
|
||||
return res.status(400).json({ success: false, error: '该用户无 OpenID' });
|
||||
}
|
||||
|
||||
const { templateId, character_string1, thing3, thing6 } = req.body;
|
||||
|
||||
const result = await WechatSubscribeService.sendVersionUpdateMessage({
|
||||
openid: user.openid,
|
||||
templateId: templateId || 'DaPPZVFsLsHzj6TJAFK5rUp8B4DW-9JOtJF00v-uLMQ',
|
||||
character_string1: character_string1 || 'v2.0',
|
||||
thing3: thing3 || '版本已更新',
|
||||
thing6: thing6 || '请查看更新内容'
|
||||
});
|
||||
|
||||
res.json({ success: result.success, data: result });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user