From 1ca73922d8ad512a4baa14920c3be3b65cb503b3 Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 20 May 2026 20:01:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AE=A1=E7=90=86=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=AF=A6=E6=83=85=E9=A1=B5=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E7=8A=B6=E6=80=81=E5=92=8C=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 /api/admin/users/:id 返回用户订阅列表 - 后端新增 POST /api/admin/users/:id/test-message 发送测试订阅消息 - 前端用户详情页展示订阅状态(只读,按类型分组) - 前端增加发送版本更新测试消息功能 --- public/admin/index.html | 78 +++++++++++++++++++++++++++++++++++++++++ src/routes/admin.js | 40 ++++++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/public/admin/index.html b/public/admin/index.html index 801e3e7..90a46df 100644 --- a/public/admin/index.html +++ b/public/admin/index.html @@ -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) {
+
+
+
加载中...
+
+
+
+ + + + + +
+
`; + if (S.subscriptions) { + renderSubscriptions(S.subscriptions); + } +} + +function renderSubscriptions(subs) { + const el = document.getElementById('subscribeStatus'); + if (!subs || subs.length === 0) { + el.innerHTML = '该用户未订阅任何通知'; + return; + } + const typeMap = {}; + subs.forEach(s => { + if (!typeMap[s.type]) typeMap[s.type] = []; + typeMap[s.type].push(s); + }); + let html = '
'; + 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 += `
+ ${label} + ${typeName} + 模板: ${latest.templateId.substring(0,8)}... + 订阅: ${fmtDate(latest.subscribedAt)} +
`; + } + html += '
'; + 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 = `✅ 发送成功 (msgid: ${res.data.messageId})`; + } else { + resultEl.innerHTML = `❌ 发送失败: errcode=${res.data.errcode} ${res.data.errmsg || ''}`; + } + } catch (e) { + resultEl.innerHTML = `❌ 异常: ${e.message}`; + } finally { + btn.disabled = false; + btn.textContent = '发送测试'; + } } function toggleVipExpire() { diff --git a/src/routes/admin.js b/src/routes/admin.js index 36ea489..73d2436 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -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); }