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); }