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