|
|
@@ -0,0 +1,917 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <title>智价云(药店版) - 活动模块API验收测试工具(签到+邀请+抽奖)</title>
|
|
|
+ <style>
|
|
|
+ * { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f0f2f5; padding: 24px; font-size: 14px; }
|
|
|
+ .container { max-width: 1400px; margin: 0 auto; }
|
|
|
+ h1 { color: #1a1a2e; margin-bottom: 8px; text-align: center; font-size: 28px; }
|
|
|
+ h2 { color: #1a1a2e; margin-bottom: 6px; text-align: center; font-size: 16px; font-weight: 400; color: #666; }
|
|
|
+ .toolbar { display: flex; gap: 12px; align-items: center; background: white; padding: 16px 20px; border-radius: 8px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); flex-wrap: wrap; }
|
|
|
+ .toolbar label { font-weight: 600; color: #333; white-space: nowrap; font-size: 14px; }
|
|
|
+ .toolbar input, .toolbar select { padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; min-width: 120px; }
|
|
|
+ .toolbar .btn { font-size: 14px; }
|
|
|
+ .login-section { background: #f8f9fa; border: 1px dashed #ccc; border-radius: 6px; padding: 12px 14px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
|
+ .login-section .login-title { font-weight: 700; font-size: 14px; color: #555; margin-right: 4px; }
|
|
|
+ .btn { display: inline-block; padding: 10px 18px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
|
|
|
+ .btn-primary { background: #2196f3; color: white; }
|
|
|
+ .btn-primary:hover { background: #1976d2; }
|
|
|
+ .btn-success { background: #4caf50; color: white; }
|
|
|
+ .btn-success:hover { background: #388e3c; }
|
|
|
+ .btn-warning { background: #ff9800; color: white; }
|
|
|
+ .btn-warning:hover { background: #f57c00; }
|
|
|
+ .btn-danger { background: #f44336; color: white; }
|
|
|
+ .btn-danger:hover { background: #d32f2f; }
|
|
|
+ .btn-outline { background: white; color: #2196f3; border: 1px solid #2196f3; }
|
|
|
+ .btn-outline:hover { background: #e3f2fd; }
|
|
|
+ .btn-sm { padding: 4px 10px; font-size: 11px; }
|
|
|
+
|
|
|
+ .module-tabs { display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
|
+ .module-tab { padding: 12px 20px; background: white; border: 1px solid #ddd; border-radius: 6px 6px 0 0; cursor: pointer; font-size: 14px; font-weight: 500; color: #666; transition: all 0.15s; border-bottom: 2px solid transparent; }
|
|
|
+ .module-tab:hover { background: #e3f2fd; color: #1976d2; }
|
|
|
+ .module-tab.active { background: white; color: #1976d2; border-bottom: 2px solid #2196f3; font-weight: 600; }
|
|
|
+
|
|
|
+ .test-section { background: white; border-radius: 8px; padding: 18px; margin-bottom: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); }
|
|
|
+ .section-title { font-size: 17px; font-weight: 600; color: #333; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #e8e8e8; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
|
+ .section-title .method { display: inline-block; padding: 3px 9px; border-radius: 3px; font-size: 12px; font-weight: 700; }
|
|
|
+ .method-get { background: #e8f5e9; color: #2e7d32; }
|
|
|
+ .method-post { background: #e3f2fd; color: #1565c0; }
|
|
|
+ .method-put { background: #fff3e0; color: #e65100; }
|
|
|
+ .method-delete { background: #fce4ec; color: #c62828; }
|
|
|
+ .section-title .path { font-family: Consolas, Monaco, monospace; color: #555; font-size: 15px; }
|
|
|
+ .section-title .auth-tag { font-size: 12px; padding: 3px 8px; border-radius: 3px; margin-left: auto; }
|
|
|
+ .auth-required { background: #ffcdd2; color: #b71c1c; }
|
|
|
+ .auth-public { background: #c8e6c9; color: #1b5e20; }
|
|
|
+ .side-tag { font-size: 12px; padding: 3px 8px; border-radius: 3px; margin-left: 4px; font-weight: 500; }
|
|
|
+ .side-admin { background: #e3f2fd; color: #1565c0; }
|
|
|
+ .side-user { background: #f3e5f5; color: #7b1fa2; }
|
|
|
+ .role-tag { font-size: 12px; padding: 3px 8px; border-radius: 3px; margin-left: 8px; font-weight: 600; background: #fff3e0; color: #e65100; }
|
|
|
+ .section-desc { font-size: 14px; color: #888; margin-bottom: 10px; }
|
|
|
+
|
|
|
+ .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-bottom: 10px; }
|
|
|
+ .form-group { margin-bottom: 8px; }
|
|
|
+ .form-group label { display: block; margin-bottom: 4px; color: #555; font-weight: 500; font-size: 14px; }
|
|
|
+ .form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 15px; }
|
|
|
+
|
|
|
+ .response-box { margin-top: 12px; background: #fafafa; border: 1px solid #e0e0e0; border-radius: 4px; padding: 12px; }
|
|
|
+ .response-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
|
|
+ .response-title { font-weight: 600; color: #333; font-size: 14px; }
|
|
|
+ .status-badge { display: inline-block; padding: 4px 10px; border-radius: 3px; font-size: 12px; font-weight: 500; }
|
|
|
+ .status-success { background: #c8e6c9; color: #2e7d32; }
|
|
|
+ .status-error { background: #ffcdd2; color: #c62828; }
|
|
|
+ .status-warn { background: #fff3e0; color: #e65100; }
|
|
|
+ .status-pending { background: #fff3e0; color: #e65100; }
|
|
|
+ .response-content { background: #263238; color: #aed581; padding: 14px; border-radius: 4px; font-family: Consolas, Monaco, monospace; font-size: 13px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; max-height: 400px; overflow-y: auto; }
|
|
|
+
|
|
|
+ .field-desc-box { margin-top: 8px; background: #f5f7fa; border: 1px solid #e0e0e0; border-radius: 4px; padding: 10px; }
|
|
|
+ .field-desc-title { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 6px; }
|
|
|
+ .field-desc-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
|
+ .field-desc-table th { background: #e8eaed; padding: 4px 8px; text-align: left; color: #333; font-weight: 600; border: 1px solid #ddd; }
|
|
|
+ .field-desc-table td { padding: 4px 8px; border: 1px solid #ddd; color: #555; }
|
|
|
+ .field-desc-table td code { background: #e8e8e8; padding: 1px 4px; border-radius: 2px; font-size: 11px; }
|
|
|
+
|
|
|
+ .summary-bar { display: flex; gap: 16px; padding: 12px 16px; background: white; border-radius: 8px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); align-items: center; flex-wrap: wrap; }
|
|
|
+ .summary-item { display: flex; align-items: center; gap: 6px; font-size: 14px; }
|
|
|
+ .summary-count { font-weight: 700; font-size: 20px; }
|
|
|
+ .summary-count.pass { color: #2e7d32; }
|
|
|
+ .summary-count.fail { color: #c62828; }
|
|
|
+ .summary-count.pending { color: #e65100; }
|
|
|
+
|
|
|
+ .batch-buttons { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
|
+
|
|
|
+ .side-switcher { display: flex; gap: 0; margin-bottom: 16px; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.12); }
|
|
|
+ .side-btn { flex: 1; padding: 16px 24px; border: none; cursor: pointer; font-size: 16px; font-weight: 600; transition: all 0.25s; text-align: center; }
|
|
|
+ .side-btn.user-side { background: #e3f2fd; color: #1565c0; }
|
|
|
+ .side-btn.user-side.active { background: #1565c0; color: white; }
|
|
|
+ .side-btn.admin-side { background: #fff3e0; color: #e65100; }
|
|
|
+ .side-btn.admin-side.active { background: #e65100; color: white; }
|
|
|
+ .side-btn:hover { opacity: 0.9; transform: translateY(-1px); }
|
|
|
+ .admin-login-area { background: #fff8e1; border: 1px solid #ffe0b2; border-radius: 6px; }
|
|
|
+ .admin-login-area .login-title { color: #e65100 !important; }
|
|
|
+ .admin-tabs .module-tab.active { color: #e65100; border-bottom-color: #e65100; }
|
|
|
+ .admin-tabs .module-tab:hover { background: #fff3e0; color: #e65100; }
|
|
|
+
|
|
|
+ @media (max-width: 768px) {
|
|
|
+ .form-grid { grid-template-columns: 1fr; }
|
|
|
+ .toolbar { flex-direction: column; align-items: stretch; }
|
|
|
+ .module-tab { font-size: 11px; padding: 8px 12px; }
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+<div class="container">
|
|
|
+ <h1>智价云(药店版) - 活动模块API验收测试工具</h1>
|
|
|
+ <h2>覆盖签到有礼 + 邀请有礼 + 签到送会员 + 比价抽奖 用户端 & 运营端 全量API | 独立登录管理 | 开发环境验证码固定 123456</h2>
|
|
|
+
|
|
|
+ <!-- 用户侧/运营侧 切换器 -->
|
|
|
+ <div class="side-switcher">
|
|
|
+ <button class="side-btn user-side active" onclick="switchSide('user')">用户侧 (C端)</button>
|
|
|
+ <button class="side-btn admin-side" onclick="switchSide('admin')">运营侧 (管理端)</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 全局工具栏 -->
|
|
|
+ <div class="toolbar">
|
|
|
+ <label>API 基础地址</label>
|
|
|
+ <label for="baseUrl"></label><input type="text" id="baseUrl" value="http://localhost:8002" style="flex:1;">
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 用户侧登录区 -->
|
|
|
+ <div id="userLoginSection">
|
|
|
+ <div class="toolbar">
|
|
|
+ <label>用户Token</label>
|
|
|
+ <label for="token"></label><input type="text" id="token" placeholder="登录后自动填入" style="flex:1;" readonly>
|
|
|
+ <span id="roleBadge" class="role-tag" style="display:none;"></span>
|
|
|
+ <button class="btn btn-outline btn-sm" onclick="copyToken()">复制Token</button>
|
|
|
+ </div>
|
|
|
+ <div class="toolbar" style="background:#f8f9fa;">
|
|
|
+ <div class="login-section">
|
|
|
+ <span class="login-title">短信登录</span>
|
|
|
+ <label>手机号</label>
|
|
|
+ <label for="phone"></label><input type="text" id="phone" value="18776156917" style="width:140px;">
|
|
|
+ <label>验证码</label>
|
|
|
+ <label for="smsCode"></label><input type="text" id="smsCode" placeholder="123456" value="123456" style="width:100px;">
|
|
|
+ <button class="btn btn-primary btn-sm" onclick="sendSms()">发送验证码</button>
|
|
|
+ <button class="btn btn-success btn-sm" onclick="smsLogin()">短信登录</button>
|
|
|
+ </div>
|
|
|
+ <div class="login-section">
|
|
|
+ <span class="login-title">密码登录</span>
|
|
|
+ <label>手机号/用户名</label>
|
|
|
+ <label for="pwdAccount"></label><input type="text" id="pwdAccount" value="18776156917" placeholder="手机号或用户名" style="width:140px;">
|
|
|
+ <label>密码</label>
|
|
|
+ <label for="pwdPassword"></label><input type="password" id="pwdPassword" placeholder="输入密码" style="width:120px;">
|
|
|
+ <button class="btn btn-success btn-sm" onclick="passwordLogin()">密码登录</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 运营侧登录区 -->
|
|
|
+ <div id="adminLoginSection" style="display:none;">
|
|
|
+ <div class="toolbar">
|
|
|
+ <label>管理员Token</label>
|
|
|
+ <label for="adminToken"></label><input type="text" id="adminToken" placeholder="管理员登录后自动填入" style="flex:1;" readonly>
|
|
|
+ <span id="adminRoleBadge" class="role-tag" style="display:none;"></span>
|
|
|
+ <button class="btn btn-outline btn-sm" onclick="copyToken()">复制Token</button>
|
|
|
+ </div>
|
|
|
+ <div class="toolbar admin-login-area">
|
|
|
+ <div class="login-section">
|
|
|
+ <span class="login-title">管理员登录(t_admin表)</span>
|
|
|
+ <label>用户名</label>
|
|
|
+ <label for="adminPwdUsername"></label><input type="text" id="adminPwdUsername" value="admin" placeholder="管理员用户名" style="width:150px;">
|
|
|
+ <label>密码</label>
|
|
|
+ <label for="adminPwdPassword"></label><input type="password" id="adminPwdPassword" value="admin@123" placeholder="管理员密码" style="width:140px;">
|
|
|
+ <button class="btn btn-warning btn-sm" onclick="adminPasswordLogin()">管理员登录</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 测试统计栏 -->
|
|
|
+ <div class="summary-bar">
|
|
|
+ <span style="font-weight:600;">本页测试统计:</span>
|
|
|
+ <div class="summary-item"><span style="font-weight:700;font-size:20px;color:#1565c0;" id="totalCount">0</span><span>总计</span></div>
|
|
|
+ <div class="summary-item"><span class="summary-count pass" id="passCount">0</span><span>通过</span></div>
|
|
|
+ <div class="summary-item"><span class="summary-count fail" id="failCount">0</span><span>失败</span></div>
|
|
|
+ <div class="summary-item"><span class="summary-count pending" id="pendingCount">0</span><span>待测</span></div>
|
|
|
+ <button class="btn btn-warning btn-sm" onclick="runAllTests()" style="margin-left:auto;">一键全部测试</button>
|
|
|
+ <button class="btn btn-outline btn-sm" onclick="clearAllResults()">清除结果</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 用户侧模块标签栏 -->
|
|
|
+ <div class="module-tabs" id="moduleTabs">
|
|
|
+ <div class="module-tab active" data-module="checkin">1.签到有礼</div>
|
|
|
+ <div class="module-tab" data-module="activity-invite">2.邀请有礼</div>
|
|
|
+ <div class="module-tab" data-module="activity-checkin">3.签到送会员</div>
|
|
|
+ <div class="module-tab" data-module="activity-lottery">4.比价抽奖</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 运营侧模块标签栏 -->
|
|
|
+ <div class="module-tabs admin-tabs" id="adminModuleTabs" style="display:none;">
|
|
|
+ <div class="module-tab active" data-module="admin-checkin">1.签到管理</div>
|
|
|
+ <div class="module-tab" data-module="admin-activity-invite">2.邀请有礼管理</div>
|
|
|
+ <div class="module-tab" data-module="admin-activity-checkin">3.签到送会员管理</div>
|
|
|
+ <div class="module-tab" data-module="admin-activity-lottery">4.比价抽奖管理</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="panels"></div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<script>
|
|
|
+// ===================== 全局状态 =====================
|
|
|
+let activeSide = 'user';
|
|
|
+const BASE_URL = () => document.getElementById('baseUrl').value;
|
|
|
+const TOKEN = () => activeSide === 'admin'
|
|
|
+ ? (document.getElementById('adminToken')?.value || '')
|
|
|
+ : (document.getElementById('token')?.value || '');
|
|
|
+const PHONE = () => document.getElementById('phone')?.value || '';
|
|
|
+const SMS_CODE = () => document.getElementById('smsCode')?.value || '123456';
|
|
|
+
|
|
|
+let stats = { pass: 0, fail: 0, pending: 0, total: 0 };
|
|
|
+
|
|
|
+const USER_MODULES = ['checkin', 'activity-invite', 'activity-checkin', 'activity-lottery'];
|
|
|
+const ADMIN_MODULES = ['admin-checkin', 'admin-activity-invite', 'admin-activity-checkin', 'admin-activity-lottery'];
|
|
|
+
|
|
|
+// ===================== 大整数安全JSON解析 =====================
|
|
|
+function safeJsonParse(text) {
|
|
|
+ if (!text || text.trim() === '' || text.trim() === 'null') return null;
|
|
|
+ const safeText = text.replace(/([:,\[]\s*)(\d{16,})(\s*[,\]\}])/g, '$1"$2"$3');
|
|
|
+ return JSON.parse(safeText);
|
|
|
+}
|
|
|
+
|
|
|
+// ===================== HTTP 请求 =====================
|
|
|
+async function api(method, path, body, requireAuth = true) {
|
|
|
+ const headers = { 'Content-Type': 'application/json' };
|
|
|
+ if (requireAuth && TOKEN()) { headers['Authorization'] = 'Bearer ' + TOKEN(); }
|
|
|
+ const opts = { method, headers };
|
|
|
+ if (body && method !== 'GET') { opts.body = JSON.stringify(body); }
|
|
|
+ const startTime = Date.now();
|
|
|
+ try {
|
|
|
+ const res = await fetch(BASE_URL() + path, opts);
|
|
|
+ const text = await res.text();
|
|
|
+ let data;
|
|
|
+ try { data = safeJsonParse(text); } catch (e) { data = { _raw: text }; }
|
|
|
+ return { ok: res.ok, status: res.status, data, duration: Date.now() - startTime };
|
|
|
+ } catch (e) {
|
|
|
+ return { ok: false, status: 0, data: { error: e.message }, duration: Date.now() - startTime };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ===================== 测试执行 =====================
|
|
|
+async function runTest(sectionId, method, path, body, requireAuth, validatorName) {
|
|
|
+ const box = document.getElementById(sectionId + '-box');
|
|
|
+ const statusEl = document.getElementById(sectionId + '-status');
|
|
|
+ const rawEl = document.getElementById(sectionId + '-raw');
|
|
|
+ const durEl = document.getElementById(sectionId + '-duration');
|
|
|
+
|
|
|
+ updateBadge(statusEl, 'pending', '运行中...');
|
|
|
+ if (box) box.style.display = 'block';
|
|
|
+
|
|
|
+ let resolvedPath = path;
|
|
|
+ document.querySelectorAll(`[id^="${sectionId}-"]`).forEach(input => {
|
|
|
+ if (input.tagName === 'INPUT') {
|
|
|
+ const fieldName = input.id.replace(sectionId + '-', '');
|
|
|
+ resolvedPath = resolvedPath.replace('{' + fieldName + '}', input.value || fieldName);
|
|
|
+ if (input.value) {
|
|
|
+ const queryVal = encodeURIComponent(input.value);
|
|
|
+ resolvedPath = resolvedPath.replace(
|
|
|
+ new RegExp('([?&])' + fieldName + '=[^&]*', 'g'),
|
|
|
+ '$1' + fieldName + '=' + queryVal
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 统计total(首次执行时计入)
|
|
|
+ if (!stats.hasOwnProperty(sectionId)) {
|
|
|
+ stats.total++;
|
|
|
+ stats.pending = Math.max(0, stats.total - stats.pass - stats.fail);
|
|
|
+ document.getElementById('totalCount').textContent = stats.total;
|
|
|
+ document.getElementById('pendingCount').textContent = stats.pending;
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await api(method, resolvedPath, body, requireAuth);
|
|
|
+ if (durEl) durEl.textContent = result.duration + 'ms';
|
|
|
+ if (rawEl) rawEl.textContent = JSON.stringify(result.data, null, 2);
|
|
|
+
|
|
|
+ let passed = false;
|
|
|
+ if (validatorName && typeof window[validatorName] === 'function') {
|
|
|
+ try { passed = window[validatorName](result); } catch (e) { passed = false; }
|
|
|
+ } else {
|
|
|
+ passed = result.ok && result.status === 200;
|
|
|
+ }
|
|
|
+
|
|
|
+ updateStats(sectionId, passed);
|
|
|
+ updateBadge(statusEl, passed ? 'success' : 'error', passed ? '通过 ' + result.status : '失败 ' + result.status);
|
|
|
+ if (box) box.style.display = 'block';
|
|
|
+}
|
|
|
+
|
|
|
+function updateBadge(el, cls, text) {
|
|
|
+ if (!el) return;
|
|
|
+ el.textContent = text;
|
|
|
+ el.className = 'status-badge status-' + cls;
|
|
|
+}
|
|
|
+
|
|
|
+function updateStats(id, passed) {
|
|
|
+ const old = stats[id];
|
|
|
+ if (old === 'pass') stats.pass--;
|
|
|
+ if (old === 'fail') stats.fail--;
|
|
|
+
|
|
|
+ stats[id] = passed ? 'pass' : 'fail';
|
|
|
+ if (passed) stats.pass++; else stats.fail++;
|
|
|
+
|
|
|
+ stats.pending = Math.max(0, stats.total - stats.pass - stats.fail);
|
|
|
+ document.getElementById('passCount').textContent = stats.pass;
|
|
|
+ document.getElementById('failCount').textContent = stats.fail;
|
|
|
+ document.getElementById('pendingCount').textContent = stats.pending;
|
|
|
+}
|
|
|
+
|
|
|
+// ===================== 登录相关 =====================
|
|
|
+async function sendSms() {
|
|
|
+ const res = await api('POST', '/api/auth/sms/send', { phone: PHONE(), scene: 'login' }, false);
|
|
|
+ if (res.ok) {
|
|
|
+ document.getElementById('smsCode').value = '123456';
|
|
|
+ alert('验证码已写入(开发测试模式,固定验证码: 123456)');
|
|
|
+ } else {
|
|
|
+ alert('发送失败: ' + JSON.stringify(res.data));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function smsLogin() {
|
|
|
+ const phone = PHONE();
|
|
|
+ const code = SMS_CODE();
|
|
|
+ if (!phone) { alert('请填写手机号'); return; }
|
|
|
+ const payload = { phone, code, scene: 'login' };
|
|
|
+ const res = await api('POST', '/api/auth/sms/login', payload, false);
|
|
|
+ if (res.data?.code === 200 && res.data?.data?.accessToken) {
|
|
|
+ document.getElementById('token').value = res.data.data.accessToken;
|
|
|
+ updateRoleBadge(res.data.data.role);
|
|
|
+ alert('短信登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'USER'));
|
|
|
+ } else {
|
|
|
+ alert('登录失败: ' + (res.data?.message || '未知错误'));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function passwordLogin() {
|
|
|
+ const account = document.getElementById('pwdAccount').value.trim();
|
|
|
+ const password = document.getElementById('pwdPassword').value;
|
|
|
+ if (!account || !password) { alert('请填写手机号/用户名和密码'); return; }
|
|
|
+ let payload;
|
|
|
+ if (/^\d{11}$/.test(account)) {
|
|
|
+ payload = { phone: account, password };
|
|
|
+ } else {
|
|
|
+ payload = { username: account, password };
|
|
|
+ }
|
|
|
+ const res = await api('POST', '/api/auth/password/login', payload, false);
|
|
|
+ if (res.data?.code === 200 && res.data?.data?.accessToken) {
|
|
|
+ document.getElementById('token').value = res.data.data.accessToken;
|
|
|
+ updateRoleBadge(res.data.data.role);
|
|
|
+ alert('密码登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'USER'));
|
|
|
+ } else {
|
|
|
+ alert('登录失败: ' + (res.data?.message || '未知错误'));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function adminPasswordLogin() {
|
|
|
+ const username = document.getElementById('adminPwdUsername').value;
|
|
|
+ const password = document.getElementById('adminPwdPassword').value;
|
|
|
+ if (!username || !password) { alert('请填写管理员用户名和密码'); return; }
|
|
|
+ const payload = { username, password };
|
|
|
+ const res = await api('POST', '/api/auth/password/login', payload, false);
|
|
|
+ if (res.data?.code === 200 && res.data?.data?.accessToken) {
|
|
|
+ document.getElementById('adminToken').value = res.data.data.accessToken;
|
|
|
+ updateAdminRoleBadge(res.data.data.role);
|
|
|
+ alert('管理员登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'ADMIN'));
|
|
|
+ } else {
|
|
|
+ alert('登录失败: ' + (res.data?.message || '未知错误'));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function updateRoleBadge(role) {
|
|
|
+ const badge = document.getElementById('roleBadge');
|
|
|
+ if (badge) {
|
|
|
+ if (role) { badge.textContent = role; badge.style.display = 'inline-block'; }
|
|
|
+ else { badge.style.display = 'none'; }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function updateAdminRoleBadge(role) {
|
|
|
+ const badge = document.getElementById('adminRoleBadge');
|
|
|
+ if (badge) {
|
|
|
+ if (role) { badge.textContent = role; badge.style.display = 'inline-block'; }
|
|
|
+ else { badge.style.display = 'none'; }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function copyToken() {
|
|
|
+ const token = TOKEN();
|
|
|
+ if (!token) { alert('当前无Token'); return; }
|
|
|
+ navigator.clipboard.writeText(token).then(() => alert('Token已复制到剪贴板'));
|
|
|
+}
|
|
|
+
|
|
|
+// ===================== 侧边切换 =====================
|
|
|
+function switchSide(side) {
|
|
|
+ activeSide = side;
|
|
|
+ const isAdmin = side === 'admin';
|
|
|
+ document.getElementById('userLoginSection').style.display = isAdmin ? 'none' : '';
|
|
|
+ document.getElementById('adminLoginSection').style.display = isAdmin ? '' : 'none';
|
|
|
+ document.getElementById('moduleTabs').style.display = isAdmin ? 'none' : '';
|
|
|
+ document.getElementById('adminModuleTabs').style.display = isAdmin ? '' : 'none';
|
|
|
+ if (isAdmin) {
|
|
|
+ const activeTab = document.querySelector('#adminModuleTabs .module-tab.active');
|
|
|
+ renderModule(activeTab ? activeTab.dataset.module : 'admin-checkin');
|
|
|
+ } else {
|
|
|
+ const activeTab = document.querySelector('#moduleTabs .module-tab.active');
|
|
|
+ renderModule(activeTab ? activeTab.dataset.module : 'checkin');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+document.getElementById('moduleTabs').addEventListener('click', (e) => {
|
|
|
+ if (!e.target.classList.contains('module-tab')) return;
|
|
|
+ document.querySelectorAll('#moduleTabs .module-tab').forEach(t => t.classList.remove('active'));
|
|
|
+ e.target.classList.add('active');
|
|
|
+ renderModule(e.target.dataset.module);
|
|
|
+});
|
|
|
+document.getElementById('adminModuleTabs').addEventListener('click', (e) => {
|
|
|
+ if (!e.target.classList.contains('module-tab')) return;
|
|
|
+ document.querySelectorAll('#adminModuleTabs .module-tab').forEach(t => t.classList.remove('active'));
|
|
|
+ e.target.classList.add('active');
|
|
|
+ renderModule(e.target.dataset.module);
|
|
|
+});
|
|
|
+
|
|
|
+// ===================== 渲染模块面板 =====================
|
|
|
+function renderModule(module) {
|
|
|
+ const panels = document.getElementById('panels');
|
|
|
+ let html = '';
|
|
|
+ switch (module) {
|
|
|
+ case 'checkin': html = renderCheckinModule(); break;
|
|
|
+ case 'activity-invite': html = renderActivityInviteModule(); break;
|
|
|
+ case 'activity-checkin': html = renderActivityCheckinModule(); break;
|
|
|
+ case 'activity-lottery': html = renderActivityLotteryModule(); break;
|
|
|
+ case 'admin-checkin': html = renderAdminCheckinModule(); break;
|
|
|
+ case 'admin-activity-invite': html = renderAdminActivityInviteModule(); break;
|
|
|
+ case 'admin-activity-checkin': html = renderAdminActivityCheckinModule(); break;
|
|
|
+ case 'admin-activity-lottery': html = renderAdminActivityLotteryModule(); break;
|
|
|
+ }
|
|
|
+ panels.innerHTML = html;
|
|
|
+ panels.querySelectorAll('input[data-raw]').forEach(input => {
|
|
|
+ input.value = input.getAttribute('data-raw');
|
|
|
+ input.removeAttribute('data-raw');
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function toValidatorName(id) {
|
|
|
+ const parts = id.split('-');
|
|
|
+ let camel = parts[0];
|
|
|
+ for (let i = 1; i < parts.length; i++) {
|
|
|
+ camel += parts[i].charAt(0).toUpperCase() + parts[i].slice(1);
|
|
|
+ }
|
|
|
+ return 'validate_' + camel;
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 字段说明数据 ====================
|
|
|
+const FIELD_DESC = {
|
|
|
+ 'resultWrapper': [
|
|
|
+ ['code', 'Integer', '业务状态码(200=成功)'],
|
|
|
+ ['message', 'String', '提示信息(如:操作成功)'],
|
|
|
+ ['data', 'Object/Array/null', '返回数据(详见下方字段)'],
|
|
|
+ ['timestamp', 'Long', '响应时间戳(毫秒级)']
|
|
|
+ ],
|
|
|
+ 'checkinStatus': [
|
|
|
+ ['data.checkedToday', 'Boolean', '今日是否已签到'],
|
|
|
+ ['data.continuousDays', 'Integer', '当前连续签到天数(今日未签到时为若签到将达成的天数-1)'],
|
|
|
+ ['data.todayReward', 'Integer', '今日签到可获奖励数量'],
|
|
|
+ ['data.nextCheckinDate', 'Date', '下一次可签到日期(今日未签到=今天)'],
|
|
|
+ ['data.enabled', 'Boolean', '签到功能是否启用']
|
|
|
+ ],
|
|
|
+ 'checkinResult': [
|
|
|
+ ['data.id', 'Long', '签到记录ID'],
|
|
|
+ ['data.checkinDate', 'Date', '签到日期'],
|
|
|
+ ['data.continuousDays', 'Integer', '签到时连续天数'],
|
|
|
+ ['data.rewardAmount', 'Integer', '本次奖励数量(爬虫次数)'],
|
|
|
+ ['data.rewardType', 'String', '奖励类型(CRAWLER)'],
|
|
|
+ ['data.createTime', 'DateTime', '签到时间']
|
|
|
+ ],
|
|
|
+ 'checkinCalendar': [
|
|
|
+ ['data.month', 'String', '年-月(如2026-07)'],
|
|
|
+ ['data.checkedDays', 'Integer', '当月已签到天数'],
|
|
|
+ ['data.continuousDays', 'Integer', '当前连续签到天数'],
|
|
|
+ ['data.checkedDates', 'Array<Date>', '已签到日期列表'],
|
|
|
+ ['data.checkedToday', 'Boolean', '今日是否已签到']
|
|
|
+ ],
|
|
|
+ 'checkinConfig': [
|
|
|
+ ['data.id', 'Long', '配置ID(固定1)'],
|
|
|
+ ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
|
|
|
+ ['data.rewardType', 'String', '奖励类型(CRAWLER)'],
|
|
|
+ ['data.baseReward', 'Integer', '基础签到奖励(每日)'],
|
|
|
+ ['data.continuousReward', 'Integer', '连续签到额外奖励'],
|
|
|
+ ['data.continuousThreshold', 'Integer', '连续奖励触发阈值(天)'],
|
|
|
+ ['data.continuousCap', 'Integer', '连续奖励累计上限'],
|
|
|
+ ['data.rewardExpireDays', 'Integer', '奖励有效天数(null=永久)'],
|
|
|
+ ['data.remark', 'String', '备注'],
|
|
|
+ ['data.createTime', 'DateTime', '创建时间'],
|
|
|
+ ['data.updateTime', 'DateTime', '更新时间']
|
|
|
+ ],
|
|
|
+ 'checkinStats': [
|
|
|
+ ['data.todayCheckinCount', 'Long', '今日签到人数'],
|
|
|
+ ['data.yesterdayCheckinCount', 'Long', '昨日签到人数'],
|
|
|
+ ['data.last7DaysCount', 'Long', '近7天签到人数(去重)'],
|
|
|
+ ['data.last30DaysCount', 'Long', '近30天签到人数(去重)'],
|
|
|
+ ['data.totalCheckinCount', 'Long', '累计签到总次数'],
|
|
|
+ ['data.totalCheckinUsers', 'Long', '累计签到用户数(去重)'],
|
|
|
+ ['data.totalRewardAmount', 'Long', '累计发放奖励次数总和'],
|
|
|
+ ['data.statDate', 'Date', '统计日期'],
|
|
|
+ ['data.enabled', 'Boolean', '签到功能是否启用']
|
|
|
+ ],
|
|
|
+ 'adminCheckinRecord': [
|
|
|
+ ['data.records[].id', 'Long', '签到记录ID'],
|
|
|
+ ['data.records[].userId', 'Long', '用户ID'],
|
|
|
+ ['data.records[].phone', 'String', '用户手机号'],
|
|
|
+ ['data.records[].nickname', 'String', '用户昵称'],
|
|
|
+ ['data.records[].pharmacyName', 'String', '药店名称'],
|
|
|
+ ['data.records[].checkinDate', 'Date', '签到日期'],
|
|
|
+ ['data.records[].continuousDays', 'Integer', '签到时连续天数'],
|
|
|
+ ['data.records[].rewardType', 'String', '奖励类型'],
|
|
|
+ ['data.records[].rewardAmount', 'Integer', '本次奖励数量'],
|
|
|
+ ['data.records[].grantId', 'Long', '关联配额发放记录ID'],
|
|
|
+ ['data.records[].clientSource', 'String', '签到端'],
|
|
|
+ ['data.records[].createTime', 'DateTime', '签到时间'],
|
|
|
+ ['data.total', 'Long', '总记录数'],
|
|
|
+ ['data.current', 'Long', '当前页'],
|
|
|
+ ['data.size', 'Long', '每页条数'],
|
|
|
+ ['data.pages', 'Long', '总页数']
|
|
|
+ ]
|
|
|
+};
|
|
|
+
|
|
|
+function renderFieldDesc(descKey) {
|
|
|
+ const fields = FIELD_DESC[descKey];
|
|
|
+ if (!fields) return '';
|
|
|
+ let rows = fields.map(f => `<tr><td><code>${f[0]}</code></td><td>${f[1]}</td><td>${f[2]}</td></tr>`).join('');
|
|
|
+ return `<div class="field-desc-box">
|
|
|
+ <div class="field-desc-title">返回字段说明</div>
|
|
|
+ <table class="field-desc-table"><tr><th>字段名</th><th>类型</th><th>说明</th></tr>${rows}</table>
|
|
|
+ </div>`;
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 测试用例区块渲染 ====================
|
|
|
+function testSection(id, title, method, methodClass, path, authTag, authClass, fields, hasValidator, descKey, desc) {
|
|
|
+ const authLabel = authTag === '需认证' ? '需认证' : '公开';
|
|
|
+ const ac = authTag === '需认证' ? 'auth-required' : 'auth-public';
|
|
|
+ const side = path.includes('/api/admin') ? 'admin' : 'user';
|
|
|
+ const sideClass = side === 'admin' ? 'side-admin' : 'side-user';
|
|
|
+ const sideLabel = side === 'admin' ? '运营侧' : '用户侧';
|
|
|
+ let fieldsHtml = '';
|
|
|
+ if (fields) {
|
|
|
+ fieldsHtml = '<div class="form-grid">';
|
|
|
+ fields.forEach(f => {
|
|
|
+ const escapedDefault = (f.default || '').replace(/"/g, '"');
|
|
|
+ const escapedPlaceholder = (f.placeholder || '').replace(/"/g, '"');
|
|
|
+ const hasRaw = f.default && f.default.includes('"');
|
|
|
+ fieldsHtml += `<div class="form-group"><label>${f.label}</label><input type="text" id="${id}-${f.name}" placeholder="${escapedPlaceholder}" value="${escapedDefault}"${hasRaw ? ` data-raw="${escapedDefault}"` : ''}></div>`;
|
|
|
+ });
|
|
|
+ fieldsHtml += '</div>';
|
|
|
+ }
|
|
|
+ const descHtml = desc ? `<div class="section-desc">${desc}</div>` : '';
|
|
|
+ const fieldDescHtml = descKey ? renderFieldDesc(descKey) : '';
|
|
|
+ return `
|
|
|
+ <div class="test-section">
|
|
|
+ <div class="section-title">
|
|
|
+ <span class="method method-${methodClass}">${method}</span>
|
|
|
+ <span class="path">${path}</span>
|
|
|
+ <span class="side-tag ${sideClass}">${sideLabel}</span>
|
|
|
+ <span class="${ac} auth-tag">${authLabel}</span>
|
|
|
+ </div>
|
|
|
+ ${descHtml}
|
|
|
+ ${fieldsHtml}
|
|
|
+ <button class="btn btn-primary" onclick="runTest('${id}', '${method}', '${path}', ${buildBody(id, fields)}, ${authTag === '公开' ? 'false' : 'true'}, ${hasValidator ? `'${toValidatorName(id)}'` : 'null'})">执行测试</button>
|
|
|
+ <div class="response-box" id="${id}-box" style="display:none;">
|
|
|
+ <div class="response-header">
|
|
|
+ <span class="response-title">响应 <span id="${id}-duration" style="color:#999;font-weight:400;"></span></span>
|
|
|
+ <span class="status-badge" id="${id}-status"></span>
|
|
|
+ </div>
|
|
|
+ <div class="response-content" id="${id}-raw"></div>
|
|
|
+ ${fieldDescHtml}
|
|
|
+ </div>
|
|
|
+ </div>`;
|
|
|
+}
|
|
|
+
|
|
|
+function buildBody(id, fields) {
|
|
|
+ if (!fields || fields.length === 0) return 'null';
|
|
|
+ let parts = [];
|
|
|
+ fields.forEach(f => {
|
|
|
+ const forceArray = f.type === 'array';
|
|
|
+ const asJson = f.type === 'json';
|
|
|
+ const forceString = f.type === 'string';
|
|
|
+ if (asJson) {
|
|
|
+ parts.push(`"${f.name}": safeJsonParse(document.getElementById('${id}-${f.name}').value)`);
|
|
|
+ } else if (forceString) {
|
|
|
+ parts.push(`"${f.name}": document.getElementById('${id}-${f.name}').value || null`);
|
|
|
+ } else {
|
|
|
+ parts.push(`"${f.name}": parseFieldValue(document.getElementById('${id}-${f.name}').value, ${forceArray})`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return `{${parts.join(',')}}`;
|
|
|
+}
|
|
|
+
|
|
|
+function parseFieldValue(val, forceArray) {
|
|
|
+ if (val === '' || val === undefined || val === null) return forceArray ? [] : null;
|
|
|
+ if (val === 'true') return forceArray ? [true] : true;
|
|
|
+ if (val === 'false') return forceArray ? [false] : false;
|
|
|
+ if (val.includes(',')) {
|
|
|
+ const items = val.split(',').map(v => v.trim()).filter(v => v !== '');
|
|
|
+ return items.map(v => toNumberIfSafe(v));
|
|
|
+ }
|
|
|
+ if (/^\d+(\.\d+)?$/.test(val)) return forceArray ? [toNumberIfSafe(val)] : toNumberIfSafe(val);
|
|
|
+ return forceArray ? [val] : val;
|
|
|
+}
|
|
|
+
|
|
|
+function toNumberIfSafe(v) {
|
|
|
+ if (!/^\d+(\.\d+)?$/.test(v)) return v;
|
|
|
+ const num = Number(v);
|
|
|
+ if (Number.isInteger(num) && !Number.isSafeInteger(num)) return v;
|
|
|
+ return num;
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 用户端:签到模块 ====================
|
|
|
+function renderCheckinModule() {
|
|
|
+ return `
|
|
|
+ <div class="batch-buttons">
|
|
|
+ <button class="btn btn-warning btn-sm" onclick="runCheckinTests()">运行全部签到测试</button>
|
|
|
+ </div>
|
|
|
+ ${testSection('checkin-status', '今日签到状态', 'GET', 'get', '/api/checkin/status', '需认证', 'auth-required', null, true, 'checkinStatus', '获取当前用户今日签到状态、连续天数、可获奖励')}
|
|
|
+ ${testSection('checkin-do', '执行签到', 'POST', 'post', '/api/checkin', '需认证', 'auth-required',
|
|
|
+ [{label:'签到端(clientSource,可选)', name:'clientSource', placeholder:'WINDOWS/ANDROID/MINIAPP', default:'WINDOWS'}], true, 'checkinResult', '执行当日签到,发放爬虫次数奖励。每个自然日仅可签到一次')}
|
|
|
+ ${testSection('checkin-calendar', '当月签到日历', 'GET', 'get', '/api/checkin/calendar?year=2026&month=7', '需认证', 'auth-required',
|
|
|
+ [{label:'年份(year,可选)', name:'year', placeholder:'2026', default:'2026'},
|
|
|
+ {label:'月份(month,可选)', name:'month', placeholder:'7', default:'7'}], true, 'checkinCalendar', '获取指定月份的签到日历,不传默认当月')}
|
|
|
+ ${testSection('checkin-records', '我的签到记录', 'GET', 'get', '/api/checkin/records?limit=30', '需认证', 'auth-required',
|
|
|
+ [{label:'条数(limit,可选)', name:'limit', placeholder:'30', default:'30'}], true, null, '获取当前用户的签到历史记录,按签到日期倒序')}`;
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 用户端:邀请有礼模块 ====================
|
|
|
+function renderActivityInviteModule() {
|
|
|
+ return `
|
|
|
+ <div class="batch-buttons">
|
|
|
+ <button class="btn btn-warning btn-sm" onclick="runActivityInviteTests()">运行全部邀请有礼测试</button>
|
|
|
+ </div>
|
|
|
+ ${testSection('activity-invite-status', '邀请有礼活动状态', 'GET', 'get', '/api/activity/invite/status', '需认证', 'auth-required', null, true, 'activityInviteStatus', '查询当前邀请有礼活动状态:是否开启、奖励模式、已获/剩余次数')}`;
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 用户端:签到送会员模块 ====================
|
|
|
+function renderActivityCheckinModule() {
|
|
|
+ return `
|
|
|
+ <div class="batch-buttons">
|
|
|
+ <button class="btn btn-warning btn-sm" onclick="runActivityCheckinTests()">运行全部签到送会员测试</button>
|
|
|
+ </div>
|
|
|
+ ${testSection('activity-checkin-status', '签到送会员周期状态', 'GET', 'get', '/api/activity/checkin-cycle/status', '需认证', 'auth-required', null, true, 'activityCheckinStatus', '查询当前周期签到进度、是否达标、累计领取次数')}`;
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 用户端:比价抽奖模块 ====================
|
|
|
+function renderActivityLotteryModule() {
|
|
|
+ return `
|
|
|
+ <div class="batch-buttons">
|
|
|
+ <button class="btn btn-warning btn-sm" onclick="runActivityLotteryTests()">运行全部比价抽奖测试</button>
|
|
|
+ </div>
|
|
|
+ ${testSection('activity-lottery-status', '比价抽奖活动状态', 'GET', 'get', '/api/activity/lottery/status', '需认证', 'auth-required', null, true, 'activityLotteryStatus', '查询抽奖活动状态:是否开启、是否满足资格、今日可用机会')}
|
|
|
+ ${testSection('activity-lottery-draw', '执行抽奖', 'POST', 'post', '/api/activity/lottery/draw', '需认证', 'auth-required', null, true, 'activityLotteryDraw', '消耗一次抽奖机会,随机抽取奖品(会员/爬虫次数/谢谢参与)')}
|
|
|
+ ${testSection('activity-lottery-records', '我的抽奖记录', 'GET', 'get', '/api/activity/lottery/records?limit=20', '需认证', 'auth-required',
|
|
|
+ [{label:'条数(limit,可选)', name:'limit', placeholder:'20', default:'20'}], true, null, '获取当前用户的抽奖历史记录')}`;
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 运营端:签到管理模块 ====================
|
|
|
+function renderAdminCheckinModule() {
|
|
|
+ return `
|
|
|
+ <div class="batch-buttons">
|
|
|
+ <button class="btn btn-warning btn-sm" onclick="runAdminCheckinTests()">运行全部签到管理测试</button>
|
|
|
+ </div>
|
|
|
+ ${testSection('admin-checkin-config', '签到配置查询', 'GET', 'get', '/api/admin/checkin/config', '需认证', 'auth-required', null, true, 'checkinConfig', '获取当前签到配置(全局唯一)')}
|
|
|
+ ${testSection('admin-checkin-config-update', '更新签到配置', 'PUT', 'put', '/api/admin/checkin/config', '需认证', 'auth-required',
|
|
|
+ [
|
|
|
+ {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
|
|
|
+ {label:'奖励类型(rewardType)', name:'rewardType', placeholder:'CRAWLER', default:'CRAWLER'},
|
|
|
+ {label:'基础奖励(baseReward)', name:'baseReward', placeholder:'1', default:'1'},
|
|
|
+ {label:'连续奖励(continuousReward)', name:'continuousReward', placeholder:'1', default:'1'},
|
|
|
+ {label:'连续阈值(continuousThreshold)', name:'continuousThreshold', placeholder:'7', default:'7'},
|
|
|
+ {label:'连续上限(continuousCap)', name:'continuousCap', placeholder:'30', default:'30'},
|
|
|
+ {label:'有效天数(rewardExpireDays)', name:'rewardExpireDays', placeholder:'30', default:'30'},
|
|
|
+ {label:'备注(remark)', name:'remark', placeholder:'默认签到配置', default:'默认签到配置:每日1次,连续7天起每日额外+1'}
|
|
|
+ ], true, 'checkinConfig', '更新签到规则配置。连续签到满阈值后每日额外发放continuousReward,累计不超过continuousCap')}
|
|
|
+ ${testSection('admin-checkin-stats', '签到统计概览', 'GET', 'get', '/api/admin/checkin/stats', '需认证', 'auth-required', null, true, 'checkinStats', '签到数据统计:今日/昨日/近7天/近30天签到人数、累计签到次数与用户数、累计发放奖励')}
|
|
|
+ ${testSection('admin-checkin-records', '签到记录分页查询', 'GET', 'get', '/api/admin/checkin/records?page=1&size=10&userId=1&startDate=2026-07-01&endDate=2026-07-07&clientSource=WINDOWS', '需认证', 'auth-required',
|
|
|
+ [{label:'页码(page)', name:'page', placeholder:'1', default:'1'},
|
|
|
+ {label:'每页条数(size)', name:'size', placeholder:'10', default:'10'},
|
|
|
+ {label:'用户ID(userId,可选)', name:'userId', placeholder:'1', default:''},
|
|
|
+ {label:'起始日期(startDate,可选)', name:'startDate', placeholder:'2026-07-01', default:''},
|
|
|
+ {label:'截止日期(endDate,可选)', name:'endDate', placeholder:'2026-07-07', default:''},
|
|
|
+ {label:'签到端(clientSource,可选)', name:'clientSource', placeholder:'WINDOWS', default:''}], true, 'adminCheckinRecord', '分页查询全局签到记录,关联用户手机号/昵称/药店名。条件均可选')}
|
|
|
+ ${testSection('admin-checkin-user-records', '指定用户签到记录', 'GET', 'get', '/api/admin/checkin/user/{userId}/records', '需认证', 'auth-required',
|
|
|
+ [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'}], true, null, '查询指定用户的全部签到记录(最多500条),含用户基础信息')}`;
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 运营端:邀请有礼管理模块 ====================
|
|
|
+function renderAdminActivityInviteModule() {
|
|
|
+ return `
|
|
|
+ <div class="batch-buttons">
|
|
|
+ <button class="btn btn-warning btn-sm" onclick="runAdminActivityInviteTests()">运行全部邀请有礼管理测试</button>
|
|
|
+ </div>
|
|
|
+ ${testSection('admin-activity-invite-config', '邀请有礼配置查询', 'GET', 'get', '/api/admin/activity/invite/config', '需认证', 'auth-required', null, true, 'adminActivityInviteConfig', '获取当前邀请有礼活动配置:启用状态、时间窗、奖励天数、触发模式、上限')}
|
|
|
+ ${testSection('admin-activity-invite-config-update', '更新邀请有礼配置', 'PUT', 'put', '/api/admin/activity/invite/config', '需认证', 'auth-required',
|
|
|
+ [
|
|
|
+ {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
|
|
|
+ {label:'开始时间(startTime)', name:'startTime', placeholder:'2026-07-10 00:00:00', default:'2026-07-10 00:00:00'},
|
|
|
+ {label:'结束时间(endTime)', name:'endTime', placeholder:'2026-07-31 23:59:59', default:'2026-07-31 23:59:59'},
|
|
|
+ {label:'邀请人奖励天数(inviterRewardDays)', name:'inviterRewardDays', placeholder:'30', default:'30'},
|
|
|
+ {label:'被邀请人奖励天数(inviteeRewardDays)', name:'inviteeRewardDays', placeholder:'30', default:'30'},
|
|
|
+ {label:'邀请人最多奖励次数(maxInviterReward)', name:'maxInviterReward', placeholder:'30', default:'30'},
|
|
|
+ {label:'奖励触发模式(rewardTrigger)', name:'rewardTrigger', placeholder:'REGISTRATION/LICENSE_APPROVED', default:'LICENSE_APPROVED'},
|
|
|
+ {label:'要求小程序用户(requireMiniappInviter)', name:'requireMiniappInviter', placeholder:'0不要求/1要求', default:'1'},
|
|
|
+ {label:'备注(remark)', name:'remark', placeholder:'邀请有礼活动', default:'邀请有礼:被邀请人资质审核通过后双方各得30天高级会员'}
|
|
|
+ ], true, 'adminActivityInviteConfig', '更新邀请有礼活动规则。rewardTrigger=REGISTRATION注册即发,LICENSE_APPROVED资质审核通过后发')}
|
|
|
+ ${testSection('admin-activity-invite-revoke', '撤销邀请奖励(反作弊)', 'POST', 'post', '/api/admin/activity/invite/revoke', '需认证', 'auth-required',
|
|
|
+ [
|
|
|
+ {label:'被邀请人ID(inviteeId)', name:'inviteeId', placeholder:'作弊者用户ID', default:''},
|
|
|
+ {label:'撤销原因(reason)', name:'reason', placeholder:'冒用他人资质', default:'冒用他人资质'}
|
|
|
+ ], true, 'adminActivityInviteRevoke', '反作弊:根据被邀请人ID撤销其对应的邀请奖励,标记为已撤销状态')}`;
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 运营端:签到送会员管理模块 ====================
|
|
|
+function renderAdminActivityCheckinModule() {
|
|
|
+ return `
|
|
|
+ <div class="batch-buttons">
|
|
|
+ <button class="btn btn-warning btn-sm" onclick="runAdminActivityCheckinTests()">运行全部签到送会员管理测试</button>
|
|
|
+ </div>
|
|
|
+ ${testSection('admin-activity-checkin-config', '签到送会员配置查询', 'GET', 'get', '/api/admin/activity/checkin-cycle/config', '需认证', 'auth-required', null, true, 'adminActivityCheckinConfig', '获取当前签到送会员活动配置:周期模式、需签到天数、奖励天数、最多领取次数')}
|
|
|
+ ${testSection('admin-activity-checkin-config-update', '更新签到送会员配置', 'PUT', 'put', '/api/admin/activity/checkin-cycle/config', '需认证', 'auth-required',
|
|
|
+ [
|
|
|
+ {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
|
|
|
+ {label:'开始时间(startTime)', name:'startTime', placeholder:'2026-07-10 00:00:00', default:'2026-07-10 00:00:00'},
|
|
|
+ {label:'结束时间(endTime)', name:'endTime', placeholder:'2026-07-31 23:59:59', default:'2026-07-31 23:59:59'},
|
|
|
+ {label:'周期模式(cycleMode)', name:'cycleMode', placeholder:'FRI_THU', default:'FRI_THU'},
|
|
|
+ {label:'需签到天数(requiredDays)', name:'requiredDays', placeholder:'4', default:'4'},
|
|
|
+ {label:'奖励天数(rewardDays)', name:'rewardDays', placeholder:'7', default:'7'},
|
|
|
+ {label:'最多领取次数(maxRewardCount)', name:'maxRewardCount', placeholder:'3', default:'3'},
|
|
|
+ {label:'备注(remark)', name:'remark', placeholder:'签到送会员', default:'每周五至下周四为一周期,累计4天签到得7天高级会员,最多3次'}
|
|
|
+ ], true, 'adminActivityCheckinConfig', '更新签到送会员活动规则。周期结束后第一天自动发放奖励')}`;
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 运营端:比价抽奖管理模块 ====================
|
|
|
+function renderAdminActivityLotteryModule() {
|
|
|
+ return `
|
|
|
+ <div class="batch-buttons">
|
|
|
+ <button class="btn btn-warning btn-sm" onclick="runAdminActivityLotteryTests()">运行全部比价抽奖管理测试</button>
|
|
|
+ </div>
|
|
|
+ ${testSection('admin-activity-lottery-config', '比价抽奖配置查询', 'GET', 'get', '/api/admin/activity/lottery/config', '需认证', 'auth-required', null, true, 'adminActivityLotteryConfig', '获取当前比价抽奖活动配置:每日机会数、所需会员等级')}
|
|
|
+ ${testSection('admin-activity-lottery-config-update', '更新比价抽奖配置', 'PUT', 'put', '/api/admin/activity/lottery/config', '需认证', 'auth-required',
|
|
|
+ [
|
|
|
+ {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
|
|
|
+ {label:'开始时间(startTime)', name:'startTime', placeholder:'2026-07-10 00:00:00', default:'2026-07-10 00:00:00'},
|
|
|
+ {label:'结束时间(endTime)', name:'endTime', placeholder:'2026-07-31 23:59:59', default:'2026-07-31 23:59:59'},
|
|
|
+ {label:'每日最多机会(dailyMaxChance)', name:'dailyMaxChance', placeholder:'1', default:'1'},
|
|
|
+ {label:'所需会员等级(requireMembershipLevel)', name:'requireMembershipLevel', placeholder:'PRO', default:'PRO'},
|
|
|
+ {label:'备注(remark)', name:'remark', placeholder:'比价抽奖', default:'高级会员每自然日首次成功比价获得1次抽奖机会'}
|
|
|
+ ], true, 'adminActivityLotteryConfig', '更新比价抽奖活动规则')}
|
|
|
+ ${testSection('admin-activity-lottery-prizes', '奖品池列表', 'GET', 'get', '/api/admin/activity/lottery/prizes', '需认证', 'auth-required', null, true, null, '获取所有奖品及其权重配置')}
|
|
|
+ ${testSection('admin-activity-lottery-prize-create', '新增奖品', 'POST', 'post', '/api/admin/activity/lottery/prizes', '需认证', 'auth-required',
|
|
|
+ [
|
|
|
+ {label:'奖品名称(name)', name:'name', placeholder:'高级会员3天', default:'爬虫次数+10'},
|
|
|
+ {label:'奖励类型(rewardType)', name:'rewardType', placeholder:'MEMBERSHIP/CRAWLER/EMPTY', default:'CRAWLER'},
|
|
|
+ {label:'奖励数量(rewardAmount)', name:'rewardAmount', placeholder:'爬虫次数填数量', default:'10'},
|
|
|
+ {label:'会员天数(rewardDays)', name:'rewardDays', placeholder:'MEMBERSHIP类型填', default:''},
|
|
|
+ {label:'会员等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO/ULTRA', default:''},
|
|
|
+ {label:'概率权重(probabilityWeight)', name:'probabilityWeight', placeholder:'权重越大越容易中', default:'5'},
|
|
|
+ {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
|
|
|
+ {label:'排序(sort)', name:'sort', placeholder:'0', default:'6'}
|
|
|
+ ], true, 'adminActivityLotteryPrize', '新增奖品到奖品池。rewardType: MEMBERSHIP会员/CRAWLER爬虫/EMPTY谢谢参与')}
|
|
|
+ ${testSection('admin-activity-lottery-prize-update', '更新奖品', 'PUT', 'put', '/api/admin/activity/lottery/prizes/{id}', '需认证', 'auth-required',
|
|
|
+ [
|
|
|
+ {label:'奖品ID(id)', name:'id', placeholder:'1', default:''},
|
|
|
+ {label:'奖品名称(name)', name:'name', placeholder:'高级会员3天', default:'爬虫次数+10'},
|
|
|
+ {label:'奖励类型(rewardType)', name:'rewardType', placeholder:'MEMBERSHIP/CRAWLER/EMPTY', default:'CRAWLER'},
|
|
|
+ {label:'奖励数量(rewardAmount)', name:'rewardAmount', placeholder:'爬虫次数填数量', default:'10'},
|
|
|
+ {label:'会员天数(rewardDays)', name:'rewardDays', placeholder:'MEMBERSHIP类型填', default:''},
|
|
|
+ {label:'会员等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO/ULTRA', default:''},
|
|
|
+ {label:'概率权重(probabilityWeight)', name:'probabilityWeight', placeholder:'权重越大越容易中', default:'5'},
|
|
|
+ {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
|
|
|
+ {label:'排序(sort)', name:'sort', placeholder:'0', default:'6'}
|
|
|
+ ], true, 'adminActivityLotteryPrize', '更新奖品池中的奖品配置')}
|
|
|
+ ${testSection('admin-activity-lottery-prize-delete', '删除奖品', 'DELETE', 'delete', '/api/admin/activity/lottery/prizes/{id}', '需认证', 'auth-required',
|
|
|
+ [{label:'奖品ID(id)', name:'id', placeholder:'需删除的奖品ID', default:''}], true, null, '从奖品池中删除指定奖品')}`;
|
|
|
+}
|
|
|
+
|
|
|
+// ===================== 批量测试 ====================
|
|
|
+function getSectionsInModule(modulePrefix) {
|
|
|
+ return Array.from(document.querySelectorAll('.test-section')).filter(s => {
|
|
|
+ const btn = s.querySelector('button[onclick*="runTest"]');
|
|
|
+ return btn && btn.getAttribute('onclick').includes("'" + modulePrefix + "-");
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function countTestsInCurrentPanel() {
|
|
|
+ return document.querySelectorAll('.test-section button[onclick*="runTest"]').length;
|
|
|
+}
|
|
|
+
|
|
|
+async function runBatchTests(modulePrefix) {
|
|
|
+ const sections = getSectionsInModule(modulePrefix);
|
|
|
+ for (const section of sections) {
|
|
|
+ const btn = section.querySelector('button[onclick*="runTest"]');
|
|
|
+ if (btn) {
|
|
|
+ const onclick = btn.getAttribute('onclick') || '';
|
|
|
+ const match = onclick.match(/runTest\('([^']+)'/);
|
|
|
+ if (match && !stats.hasOwnProperty(match[1])) {
|
|
|
+ stats.total++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ stats.pending = Math.max(0, stats.total - stats.pass - stats.fail);
|
|
|
+ document.getElementById('totalCount').textContent = stats.total;
|
|
|
+ document.getElementById('pendingCount').textContent = stats.pending;
|
|
|
+ for (const section of sections) {
|
|
|
+ const btn = section.querySelector('button[onclick*="runTest"]');
|
|
|
+ if (btn) {
|
|
|
+ btn.click();
|
|
|
+ await new Promise(r => setTimeout(r, 600));
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function runAllTests() {
|
|
|
+ resetStats();
|
|
|
+ for (const m of USER_MODULES) {
|
|
|
+ const tab = document.querySelector(`#moduleTabs .module-tab[data-module="${m}"]`);
|
|
|
+ if (tab) tab.click();
|
|
|
+ await new Promise(r => setTimeout(r, 50));
|
|
|
+ stats.total += countTestsInCurrentPanel();
|
|
|
+ }
|
|
|
+ for (const m of ADMIN_MODULES) {
|
|
|
+ const tab = document.querySelector(`#adminModuleTabs .module-tab[data-module="${m}"]`);
|
|
|
+ if (tab) tab.click();
|
|
|
+ await new Promise(r => setTimeout(r, 50));
|
|
|
+ stats.total += countTestsInCurrentPanel();
|
|
|
+ }
|
|
|
+ document.getElementById('totalCount').textContent = stats.total;
|
|
|
+ stats.pending = stats.total;
|
|
|
+ document.getElementById('pendingCount').textContent = stats.pending;
|
|
|
+
|
|
|
+ switchSide('user');
|
|
|
+ for (const m of USER_MODULES) {
|
|
|
+ try {
|
|
|
+ const tab = document.querySelector(`#moduleTabs .module-tab[data-module="${m}"]`);
|
|
|
+ if (tab) tab.click();
|
|
|
+ await new Promise(r => setTimeout(r, 100));
|
|
|
+ await runBatchTests(m);
|
|
|
+ } catch (e) { console.error('模块 ' + m + ' 测试出错:', e); }
|
|
|
+ }
|
|
|
+ switchSide('admin');
|
|
|
+ for (const m of ADMIN_MODULES) {
|
|
|
+ try {
|
|
|
+ const tab = document.querySelector(`#adminModuleTabs .module-tab[data-module="${m}"]`);
|
|
|
+ if (tab) tab.click();
|
|
|
+ await new Promise(r => setTimeout(r, 100));
|
|
|
+ await runBatchTests(m);
|
|
|
+ } catch (e) { console.error('模块 ' + m + ' 测试出错:', e); }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function resetStats() {
|
|
|
+ const keys = Object.keys(stats);
|
|
|
+ for (const k of keys) {
|
|
|
+ if (k !== 'pass' && k !== 'fail' && k !== 'pending' && k !== 'total') {
|
|
|
+ delete stats[k];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ stats.pass = 0;
|
|
|
+ stats.fail = 0;
|
|
|
+ stats.pending = 0;
|
|
|
+ stats.total = 0;
|
|
|
+ document.getElementById('totalCount').textContent = '0';
|
|
|
+ document.getElementById('passCount').textContent = '0';
|
|
|
+ document.getElementById('failCount').textContent = '0';
|
|
|
+ document.getElementById('pendingCount').textContent = '0';
|
|
|
+}
|
|
|
+
|
|
|
+function clearAllResults() {
|
|
|
+ document.querySelectorAll('.response-box').forEach(b => b.style.display = 'none');
|
|
|
+ document.querySelectorAll('.status-badge').forEach(b => { b.textContent = ''; b.className = 'status-badge'; });
|
|
|
+ resetStats();
|
|
|
+}
|
|
|
+
|
|
|
+// 用户侧/运营侧快捷批量运行
|
|
|
+function runCheckinTests() { runBatchTests('checkin'); }
|
|
|
+function runActivityInviteTests() { runBatchTests('activity-invite'); }
|
|
|
+function runActivityCheckinTests() { runBatchTests('activity-checkin'); }
|
|
|
+function runActivityLotteryTests() { runBatchTests('activity-lottery'); }
|
|
|
+function runAdminCheckinTests() { runBatchTests('admin-checkin'); }
|
|
|
+function runAdminActivityInviteTests() { runBatchTests('admin-activity-invite'); }
|
|
|
+function runAdminActivityCheckinTests() { runBatchTests('admin-activity-checkin'); }
|
|
|
+function runAdminActivityLotteryTests() { runBatchTests('admin-activity-lottery'); }
|
|
|
+
|
|
|
+// ===================== 验证器 =====================
|
|
|
+function validate_checkinStatus(result) { return result.data?.code === 200 && result.data?.data?.checkedToday !== undefined; }
|
|
|
+function validate_checkinDo(result) { return result.data?.code === 200 && result.data?.data?.id; }
|
|
|
+function validate_checkinCalendar(result) { return result.data?.code === 200 && result.data?.data?.checkedDates !== undefined; }
|
|
|
+function validate_checkinRecords(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
|
|
|
+function validate_adminCheckinConfig(result) { return result.data?.code === 200 && result.data?.data?.id; }
|
|
|
+function validate_adminCheckinConfigUpdate(result) { return result.data?.code === 200 && result.data?.data?.id; }
|
|
|
+function validate_adminCheckinStats(result) { return result.data?.code === 200 && result.data?.data?.todayCheckinCount !== undefined; }
|
|
|
+function validate_adminCheckinRecords(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
|
|
|
+function validate_adminCheckinUserRecords(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
|
|
|
+
|
|
|
+function validate_activityInviteStatus(result) { return result.data?.code === 200 && result.data?.data?.active !== undefined; }
|
|
|
+function validate_activityCheckinStatus(result) { return result.data?.code === 200 && result.data?.data?.active !== undefined; }
|
|
|
+function validate_activityLotteryStatus(result) { return result.data?.code === 200 && result.data?.data?.active !== undefined; }
|
|
|
+function validate_activityLotteryDraw(result) { return result.data?.code === 200 && result.data?.data?.prizeName !== undefined; }
|
|
|
+function validate_adminActivityInviteConfig(result) { return result.data?.code === 200 && result.data?.data?.id; }
|
|
|
+function validate_adminActivityInviteRevoke(result) { return result.data?.code === 200 && result.data?.data !== undefined; }
|
|
|
+function validate_adminActivityCheckinConfig(result) { return result.data?.code === 200 && result.data?.data?.id; }
|
|
|
+function validate_adminActivityLotteryConfig(result) { return result.data?.code === 200 && result.data?.data?.id; }
|
|
|
+function validate_adminActivityLotteryPrize(result) { return result.data?.code === 200 && result.data?.data?.id; }
|
|
|
+
|
|
|
+// ===================== 初始化 =====================
|
|
|
+window.addEventListener('DOMContentLoaded', () => {
|
|
|
+ const params = new URLSearchParams(window.location.search);
|
|
|
+ if (params.get('baseUrl')) document.getElementById('baseUrl').value = params.get('baseUrl');
|
|
|
+ renderModule('checkin');
|
|
|
+});
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+</html>
|