feat: 管理后台用户详情页增加订阅状态和测试消息发送
- 后端新增 /api/admin/users/:id 返回用户订阅列表 - 后端新增 POST /api/admin/users/:id/test-message 发送测试订阅消息 - 前端用户详情页展示订阅状态(只读,按类型分组) - 前端增加发送版本更新测试消息功能
This commit is contained in:
@@ -67,6 +67,9 @@ tr:hover td { background: #f8f9fa; }
|
|||||||
.badge-vip { background: #e8f0fe; color: #1967d2; }
|
.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 { 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-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 */
|
||||||
.pagination { padding: 16px 20px; display: flex; justify-content: center; align-items: center; gap: 12px; }
|
.pagination { padding: 16px 20px; display: flex; justify-content: center; align-items: center; gap: 12px; }
|
||||||
@@ -355,6 +358,7 @@ async function openModal(id) {
|
|||||||
try {
|
try {
|
||||||
const d = await api('/api/admin/users/' + id);
|
const d = await api('/api/admin/users/' + id);
|
||||||
S.editingUser = d.data;
|
S.editingUser = d.data;
|
||||||
|
S.subscriptions = d.subscriptions || [];
|
||||||
renderModal(d.data);
|
renderModal(d.data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('加载用户详情失败', false);
|
showToast('加载用户详情失败', false);
|
||||||
@@ -404,7 +408,81 @@ function renderModal(u) {
|
|||||||
<div class="field-row">
|
<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-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 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>`;
|
</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() {
|
function toggleVipExpire() {
|
||||||
|
|||||||
+39
-1
@@ -1,6 +1,8 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
|
const UserSubscription = require('../models/UserSubscription');
|
||||||
|
const WechatSubscribeService = require('../services/wechatSubscribeService');
|
||||||
const { adminAuth } = require('../middleware/adminAuth');
|
const { adminAuth } = require('../middleware/adminAuth');
|
||||||
|
|
||||||
router.post('/login', (req, res) => {
|
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: '用户不存在' });
|
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) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user