| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114 |
- <!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; }
- .field-desc-details { margin-bottom: 12px; background: #f5f7fa; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px 12px; }
- .field-desc-summary { cursor: pointer; font-size: 13px; font-weight: 600; color: #555; user-select: none; padding: 2px 0; }
- .field-desc-summary:hover { color: #1976d2; }
- .curl-example { background: #1e1e1e; color: #dcdcaa; padding: 12px 14px; border-radius: 4px; font-family: Consolas, Monaco, 'Courier New', monospace; font-size: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; margin: 6px 0 0 0; line-height: 1.6; }
- .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', '当前连续签到天数'],
- ['data.nextCheckinDate', 'Date', '下一次可签到日期'],
- ['data.enabled', 'Boolean', '签到功能是否启用'],
- ['data.currentPeriodId', 'Long', '当前周期ID(无周期时为null)'],
- ['data.cycleMode', 'String', '周期模式(FRI_THU/MON_SUN)'],
- ['data.periodStartDate', 'Date', '当前周期起始日'],
- ['data.periodEndDate', 'Date', '当前周期截止日'],
- ['data.periodRequiredDays', 'Integer', '周期需签到天数'],
- ['data.rewardMode', 'String', '达标模式:CUMULATIVE-累计/CONSECUTIVE-连续'],
- ['data.periodCheckinCount', 'Integer', '当前周期已签到天数'],
- ['data.periodReachedTarget', 'Boolean', '当前周期是否已达标'],
- ['data.periodRewardDays', 'Integer', '达标可获会员天数'],
- ['data.rewardedCount', 'Long', '累计已领取奖励次数'],
- ['data.maxRewardCount', 'Integer', '活动期间最多可领取次数'],
- ['data.remainingCount', 'Long', '剩余可领取次数']
- ],
- 'checkinResult': [
- ['data.id', 'Long', '签到记录ID'],
- ['data.checkinDate', 'Date', '签到日期'],
- ['data.continuousDays', 'Integer', '签到时连续天数'],
- ['data.rewardAmount', 'Integer', '本次奖励(统一0,周期达标后才发奖)'],
- ['data.rewardType', 'String', '奖励类型(MEMBERSHIP)'],
- ['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.startTime', 'DateTime', '活动开始时间'],
- ['data.endTime', 'DateTime', '活动结束时间'],
- ['data.rewardType', 'String', '奖励类型(MEMBERSHIP-会员天数)'],
- ['data.rewardLevel', 'String', '奖励会员等级(PRO/ULTRA)'],
- ['data.cycleMode', 'String', '周期模式默认值(FRI_THU/MON_SUN)'],
- ['data.requiredDays', 'Integer', '周期需签到天数默认值'],
- ['data.rewardMode', 'String', '达标模式默认值:CUMULATIVE-累计/CONSECUTIVE-连续'],
- ['data.rewardDays', 'Integer', '周期达标奖励天数默认值'],
- ['data.maxRewardCount', 'Integer', '活动期间每用户最多领取次数'],
- ['data.remark', 'String', '备注'],
- ['data.createTime', 'DateTime', '创建时间'],
- ['data.updateTime', 'DateTime', '更新时间']
- ],
- 'checkinPeriod': [
- ['data[].id', 'Long', '周期ID'],
- ['data[].periodName', 'String', '周期名称'],
- ['data[].cycleMode', 'String', '周期模式(FRI_THU/MON_SUN)'],
- ['data[].startDate', 'Date', '周期起始日'],
- ['data[].endDate', 'Date', '周期截止日'],
- ['data[].requiredDays', 'Integer', '需签到天数'],
- ['data[].rewardMode', 'String', '达标模式:CUMULATIVE/CONSECUTIVE'],
- ['data[].rewardDays', 'Integer', '奖励会员天数'],
- ['data[].rewardLevel', 'String', '奖励会员等级'],
- ['data[].status', 'String', 'PENDING-待开始/ACTIVE-进行中/CLOSED-已结束'],
- ['data[].sort', 'Integer', '排序']
- ],
- '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[].clientSource', 'String', '签到端'],
- ['data.records[].createTime', 'DateTime', '签到时间'],
- ['data.total', 'Long', '总记录数'],
- ['data.current', 'Long', '当前页'],
- ['data.size', 'Long', '每页条数'],
- ['data.pages', 'Long', '总页数']
- ],
- 'activityInviteStatus': [
- ['data.enabled', 'Boolean', '活动是否启用'],
- ['data.inWindow', 'Boolean', '是否在活动时间窗内'],
- ['data.rewardedCount', 'Long', '已获奖励次数'],
- ['data.maxInviterReward', 'Integer', '邀请人最多奖励次数'],
- ['data.remainingCount', 'Long', '剩余可获奖励次数']
- ],
- 'activityCheckinStatus': [
- ['data.enabled', 'Boolean', '活动是否启用'],
- ['data.inWindow', 'Boolean', '是否在活动时间窗内'],
- ['data.currentCycleStart', 'Date', '当前周期起始日(周五)'],
- ['data.currentCycleEnd', 'Date', '当前周期截止日(下周四)'],
- ['data.currentCycleCheckinCount', 'Integer', '当前周期内已签到天数'],
- ['data.requiredDays', 'Integer', '达标所需签到天数'],
- ['data.reachedTarget', 'Boolean', '当前周期是否已达标'],
- ['data.rewardDays', 'Integer', '达标后可获会员天数'],
- ['data.rewardedCount', 'Long', '已领取奖励次数'],
- ['data.maxRewardCount', 'Integer', '最多可领取奖励次数'],
- ['data.remainingCount', 'Long', '剩余可领取奖励次数']
- ],
- 'activityLotteryStatus': [
- ['data.enabled', 'Boolean', '活动是否启用'],
- ['data.inWindow', 'Boolean', '是否在活动时间窗内'],
- ['data.eligible', 'Boolean', '是否满足参与资格(会员等级)'],
- ['data.membershipLevel', 'String', '当前用户会员等级'],
- ['data.requireMembershipLevel', 'String', '所需会员等级'],
- ['data.availableChances', 'Integer', '今日可用抽奖机会数'],
- ['data.usedChances', 'Integer', '今日已用抽奖机会数']
- ],
- 'activityLotteryPrizes': [
- ['data[].id', 'Long', '奖品ID'],
- ['data[].name', 'String', '奖品名称'],
- ['data[].rewardType', 'String', '奖励类型(MEMBERSHIP会员/CRAWLER爬虫次数/EMPTY谢谢参与)'],
- ['data[].rewardDays', 'Integer', '会员奖励天数'],
- ['data[].rewardLevel', 'String', '会员奖励等级(PRO/ULTRA)'],
- ['data[].rewardAmount', 'Integer', '爬虫奖励次数(CRAWLER类型时有效,暂未实现)'],
- ['data[].sort', 'Integer', '排序']
- ],
- 'activityLotteryDraw': [
- ['data.recordId', 'Long', '抽奖记录ID'],
- ['data.prizeName', 'String', '奖品名称'],
- ['data.rewardType', 'String', '奖励类型(MEMBERSHIP会员/CRAWLER爬虫次数/EMPTY谢谢参与)'],
- ['data.rewardAmount', 'Integer', '奖励数量(预留)'],
- ['data.rewardDays', 'Integer', '会员奖励天数'],
- ['data.rewardLevel', 'String', '会员奖励等级(PRO/ULTRA)'],
- ['data.win', 'Boolean', '是否中奖(EMPTY=未中奖)'],
- ['data.createTime', 'DateTime', '抽奖时间']
- ],
- 'adminActivityInviteConfig': [
- ['data.id', 'Long', '配置ID(固定1)'],
- ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
- ['data.startTime', 'DateTime', '活动开始时间'],
- ['data.endTime', 'DateTime', '活动结束时间'],
- ['data.inviterRewardDays', 'Integer', '邀请人奖励天数'],
- ['data.inviteeRewardDays', 'Integer', '被邀请人奖励天数'],
- ['data.maxInviterReward', 'Integer', '邀请人最多奖励次数'],
- ['data.rewardTrigger', 'String', '奖励触发模式(REGISTRATION/LICENSE_APPROVED)'],
- ['data.requireMiniappInviter', 'Integer', '是否要求邀请人为小程序用户:0-否 1-是'],
- ['data.remark', 'String', '备注'],
- ['data.createTime', 'DateTime', '创建时间'],
- ['data.updateTime', 'DateTime', '更新时间']
- ],
- 'adminActivityInviteRevoke': [
- ['data', 'Integer', '撤销的奖励记录数']
- ],
- 'adminActivityCheckinConfig': [
- ['data.id', 'Long', '配置ID(固定1)'],
- ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
- ['data.startTime', 'DateTime', '活动开始时间'],
- ['data.endTime', 'DateTime', '活动结束时间'],
- ['data.cycleMode', 'String', '周期模式(FRI_THU=周五至下周四)'],
- ['data.requiredDays', 'Integer', '达标所需签到天数'],
- ['data.rewardDays', 'Integer', '达标奖励会员天数'],
- ['data.maxRewardCount', 'Integer', '每人最多可领取奖励次数'],
- ['data.remark', 'String', '备注'],
- ['data.createTime', 'DateTime', '创建时间'],
- ['data.updateTime', 'DateTime', '更新时间']
- ],
- 'adminActivityLotteryConfig': [
- ['data.id', 'Long', '配置ID(固定1)'],
- ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
- ['data.startTime', 'DateTime', '活动开始时间'],
- ['data.endTime', 'DateTime', '活动结束时间'],
- ['data.dailyMaxChance', 'Integer', '每个自然日最多获得抽奖机会数'],
- ['data.requireMembershipLevel', 'String', '参与所需会员等级(PRO/ULTRA/PLUS)'],
- ['data.remark', 'String', '备注'],
- ['data.createTime', 'DateTime', '创建时间'],
- ['data.updateTime', 'DateTime', '更新时间']
- ],
- 'adminActivityLotteryPrize': [
- ['data.id', 'Long', '奖品ID'],
- ['data.name', 'String', '奖品名称'],
- ['data.rewardType', 'String', '奖励类型(MEMBERSHIP会员/CRAWLER爬虫次数/EMPTY谢谢参与)'],
- ['data.rewardAmount', 'Integer', '奖励数量(预留)'],
- ['data.rewardDays', 'Integer', '会员奖励天数'],
- ['data.rewardLevel', 'String', '会员奖励等级'],
- ['data.probabilityWeight', 'Integer', '中奖概率权重(越大越容易中)'],
- ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
- ['data.sort', 'Integer', '排序(数字越小越靠前)']
- ]
- };
- 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 `<details open class="field-desc-details">
- <summary class="field-desc-summary">📋 返回字段说明</summary>
- <table class="field-desc-table"><tr><th>字段名</th><th>类型</th><th>说明</th></tr>${rows}</table>
- </details>`;
- }
- // ==================== 调用示例生成 ====================
- function renderCurlExample(id, method, path, fields) {
- const methodUpper = method.toUpperCase();
- const baseUrl = document.getElementById('baseUrl').value;
- let curl = `curl -X ${methodUpper} "${baseUrl}${path}"`;
- curl += ` \\\n -H "Authorization: Bearer \${TOKEN}"`;
- if (methodUpper === 'POST' || methodUpper === 'PUT') {
- curl += ` \\\n -H "Content-Type: application/json"`;
- }
- if (fields && fields.length > 0) {
- let bodyParts = [];
- fields.forEach(f => {
- let val = f.default || f.placeholder || '';
- bodyParts.push(`"${f.name}": "${val}"`);
- });
- if (bodyParts.length > 0) {
- curl += ` \\\n -d '{${bodyParts.join(', ')}}'`;
- }
- }
- return `<details class="field-desc-details">
- <summary class="field-desc-summary">📞 cURL 调用示例</summary>
- <pre class="curl-example">${curl}</pre>
- </details>`;
- }
- // ==================== 测试用例区块渲染 ====================
- 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) : '';
- const curlHtml = renderCurlExample(id, method, path, fields);
- 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}
- ${curlHtml}
- ${fieldDescHtml}
- ${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>
- </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', '【旧版】查询当前周期签到进度。新版签到状态已整合周期信息到 /api/checkin/status')}`;
- }
- // ==================== 用户端:比价抽奖模块 ====================
- function renderActivityLotteryModule() {
- return `
- <div class="batch-buttons">
- <button class="btn btn-warning btn-sm" onclick="runActivityLotteryTests()">运行全部比价抽奖测试</button>
- </div>
- ${testSection('activity-lottery-prizes', '奖励信息(可抽奖品列表)', 'GET', 'get', '/api/activity/lottery/prizes', '需认证', 'auth-required', null, true, 'activityLotteryPrizes', '查看当前可抽奖品列表(仅启用奖品,不显示概率权重等敏感信息)')}
- ${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:'开始时间(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:'会员等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO/ULTRA', default:'PRO'},
- {label:'周期模式(cycleMode)', name:'cycleMode', placeholder:'FRI_THU/MON_SUN', default:'FRI_THU'},
- {label:'需签到天数(requiredDays)', name:'requiredDays', placeholder:'4', default:'4'},
- {label:'达标模式(rewardMode)', name:'rewardMode', placeholder:'CUMULATIVE/CONSECUTIVE', default:'CUMULATIVE'},
- {label:'奖励天数(rewardDays)', name:'rewardDays', placeholder:'7', default:'7'},
- {label:'最多领取次数(maxRewardCount)', name:'maxRewardCount', placeholder:'3', default:'3'},
- {label:'备注(remark)', name:'remark', placeholder:'签到送会员', default:'签到送会员:每周期累计签到达标后发放奖励'}
- ], true, 'checkinConfig', '更新签到全局配置。rewardMode: CUMULATIVE-周期内累计签到N天 / CONSECUTIVE-周期内连续签到N天')}
- ${testSection('admin-checkin-periods', '周期列表', 'GET', 'get', '/api/admin/checkin/periods', '需认证', 'auth-required', null, true, 'checkinPeriod', '获取所有签到周期,每个周期可独立配置起止日期、需签到天数、达标模式、奖励')}
- ${testSection('admin-checkin-period-create', '创建周期', 'POST', 'post', '/api/admin/checkin/periods', '需认证', 'auth-required',
- [
- {label:'周期名称(periodName)', name:'periodName', placeholder:'第N周', default:'测试周期'},
- {label:'周期模式(cycleMode)', name:'cycleMode', placeholder:'FRI_THU', default:'FRI_THU'},
- {label:'起始日(startDate)', name:'startDate', placeholder:'2026-07-10', default:'2026-07-10'},
- {label:'截止日(endDate)', name:'endDate', placeholder:'2026-07-16', default:'2026-07-16'},
- {label:'需签到天数(requiredDays)', name:'requiredDays', placeholder:'4', default:'4'},
- {label:'达标模式(rewardMode)', name:'rewardMode', placeholder:'CUMULATIVE/CONSECUTIVE', default:'CUMULATIVE'},
- {label:'奖励天数(rewardDays)', name:'rewardDays', placeholder:'7', default:'7'},
- {label:'奖励等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO', default:'PRO'},
- {label:'排序(sort)', name:'sort', placeholder:'0', default:'99'}
- ], true, 'checkinPeriod', '创建一个签到周期。rewardMode: CUMULATIVE累计/CONSECUTIVE连续')}
- ${testSection('admin-checkin-period-update', '更新周期', 'PUT', 'put', '/api/admin/checkin/periods/{id}', '需认证', 'auth-required',
- [
- {label:'周期ID(id)', name:'id', placeholder:'1', default:''},
- {label:'周期名称(periodName)', name:'periodName', placeholder:'第N周', default:''},
- {label:'周期模式(cycleMode)', name:'cycleMode', placeholder:'FRI_THU', default:''},
- {label:'起始日(startDate)', name:'startDate', placeholder:'2026-07-10', default:''},
- {label:'截止日(endDate)', name:'endDate', placeholder:'2026-07-16', default:''},
- {label:'需签到天数(requiredDays)', name:'requiredDays', placeholder:'4', default:''},
- {label:'达标模式(rewardMode)', name:'rewardMode', placeholder:'CUMULATIVE/CONSECUTIVE', default:''},
- {label:'奖励天数(rewardDays)', name:'rewardDays', placeholder:'7', default:''},
- {label:'奖励等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO', default:''},
- {label:'排序(sort)', name:'sort', placeholder:'0', default:''}
- ], true, 'checkinPeriod', '更新签到周期配置')}
- ${testSection('admin-checkin-period-delete', '删除周期', 'DELETE', 'delete', '/api/admin/checkin/periods/{id}', '需认证', 'auth-required',
- [{label:'周期ID(id)', name:'id', placeholder:'需删除的周期ID', default:''}], true, null, '删除指定签到周期')}
- ${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', '【旧版】获取签到送会员活动独立配置。新版签到体系使用 /api/admin/checkin/config + 周期表管理')}
- ${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:'高级会员3天'},
- {label:'奖励类型(rewardType)', name:'rewardType', placeholder:'MEMBERSHIP/CRAWLER/EMPTY', default:'MEMBERSHIP'},
- {label:'会员天数(rewardDays)', name:'rewardDays', placeholder:'3', default:'3'},
- {label:'会员等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO/ULTRA', default:'PRO'},
- {label:'爬虫次数(rewardAmount)', name:'rewardAmount', placeholder:'CRAWLER类型时填写', default:'0'},
- {label:'概率权重(probabilityWeight)', name:'probabilityWeight', placeholder:'权重越大越容易中', default:'20'},
- {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
- {label:'排序(sort)', name:'sort', placeholder:'0', default:'3'}
- ], 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:'高级会员3天'},
- {label:'奖励类型(rewardType)', name:'rewardType', placeholder:'MEMBERSHIP/CRAWLER/EMPTY', default:'MEMBERSHIP'},
- {label:'会员天数(rewardDays)', name:'rewardDays', placeholder:'3', default:'3'},
- {label:'会员等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO/ULTRA', default:'PRO'},
- {label:'爬虫次数(rewardAmount)', name:'rewardAmount', placeholder:'CRAWLER类型时填写', default:'0'},
- {label:'概率权重(probabilityWeight)', name:'probabilityWeight', placeholder:'权重越大越容易中', default:'20'},
- {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
- {label:'排序(sort)', name:'sort', placeholder:'0', default:'3'}
- ], 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_adminCheckinPeriods(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
- function validate_adminCheckinPeriodCreate(result) { return result.data?.code === 200 && result.data?.data?.id; }
- function validate_adminCheckinPeriodUpdate(result) { return result.data?.code === 200 && result.data?.data?.id; }
- function validate_adminCheckinPeriodDelete(result) { return result.data?.code === 200; }
- 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?.enabled !== undefined; }
- function validate_activityCheckinStatus(result) { return result.data?.code === 200 && result.data?.data?.enabled !== undefined; }
- function validate_activityLotteryStatus(result) { return result.data?.code === 200 && result.data?.data?.enabled !== undefined; }
- function validate_activityLotteryPrizes(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
- 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; }
- function validate_adminActivityLotteryPrizeCreate(result) { return result.data?.code === 200 && result.data?.data?.id; }
- function validate_adminActivityLotteryPrizeUpdate(result) { return result.data?.code === 200 && result.data?.data?.id; }
- function validate_adminActivityLotteryPrizeDelete(result) { return result.data?.code === 200; }
- // ===================== 初始化 =====================
- 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>
|