activity-test.html 69 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>智价云(药店版) - 活动模块API验收测试工具(签到+邀请+抽奖)</title>
  7. <style>
  8. * { margin: 0; padding: 0; box-sizing: border-box; }
  9. body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f0f2f5; padding: 24px; font-size: 14px; }
  10. .container { max-width: 1400px; margin: 0 auto; }
  11. h1 { color: #1a1a2e; margin-bottom: 8px; text-align: center; font-size: 28px; }
  12. h2 { color: #1a1a2e; margin-bottom: 6px; text-align: center; font-size: 16px; font-weight: 400; color: #666; }
  13. .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; }
  14. .toolbar label { font-weight: 600; color: #333; white-space: nowrap; font-size: 14px; }
  15. .toolbar input, .toolbar select { padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; min-width: 120px; }
  16. .toolbar .btn { font-size: 14px; }
  17. .login-section { background: #f8f9fa; border: 1px dashed #ccc; border-radius: 6px; padding: 12px 14px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
  18. .login-section .login-title { font-weight: 700; font-size: 14px; color: #555; margin-right: 4px; }
  19. .btn { display: inline-block; padding: 10px 18px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
  20. .btn-primary { background: #2196f3; color: white; }
  21. .btn-primary:hover { background: #1976d2; }
  22. .btn-success { background: #4caf50; color: white; }
  23. .btn-success:hover { background: #388e3c; }
  24. .btn-warning { background: #ff9800; color: white; }
  25. .btn-warning:hover { background: #f57c00; }
  26. .btn-danger { background: #f44336; color: white; }
  27. .btn-danger:hover { background: #d32f2f; }
  28. .btn-outline { background: white; color: #2196f3; border: 1px solid #2196f3; }
  29. .btn-outline:hover { background: #e3f2fd; }
  30. .btn-sm { padding: 4px 10px; font-size: 11px; }
  31. .module-tabs { display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap; }
  32. .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; }
  33. .module-tab:hover { background: #e3f2fd; color: #1976d2; }
  34. .module-tab.active { background: white; color: #1976d2; border-bottom: 2px solid #2196f3; font-weight: 600; }
  35. .test-section { background: white; border-radius: 8px; padding: 18px; margin-bottom: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); }
  36. .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; }
  37. .section-title .method { display: inline-block; padding: 3px 9px; border-radius: 3px; font-size: 12px; font-weight: 700; }
  38. .method-get { background: #e8f5e9; color: #2e7d32; }
  39. .method-post { background: #e3f2fd; color: #1565c0; }
  40. .method-put { background: #fff3e0; color: #e65100; }
  41. .method-delete { background: #fce4ec; color: #c62828; }
  42. .section-title .path { font-family: Consolas, Monaco, monospace; color: #555; font-size: 15px; }
  43. .section-title .auth-tag { font-size: 12px; padding: 3px 8px; border-radius: 3px; margin-left: auto; }
  44. .auth-required { background: #ffcdd2; color: #b71c1c; }
  45. .auth-public { background: #c8e6c9; color: #1b5e20; }
  46. .side-tag { font-size: 12px; padding: 3px 8px; border-radius: 3px; margin-left: 4px; font-weight: 500; }
  47. .side-admin { background: #e3f2fd; color: #1565c0; }
  48. .side-user { background: #f3e5f5; color: #7b1fa2; }
  49. .role-tag { font-size: 12px; padding: 3px 8px; border-radius: 3px; margin-left: 8px; font-weight: 600; background: #fff3e0; color: #e65100; }
  50. .section-desc { font-size: 14px; color: #888; margin-bottom: 10px; }
  51. .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-bottom: 10px; }
  52. .form-group { margin-bottom: 8px; }
  53. .form-group label { display: block; margin-bottom: 4px; color: #555; font-weight: 500; font-size: 14px; }
  54. .form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 15px; }
  55. .response-box { margin-top: 12px; background: #fafafa; border: 1px solid #e0e0e0; border-radius: 4px; padding: 12px; }
  56. .response-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
  57. .response-title { font-weight: 600; color: #333; font-size: 14px; }
  58. .status-badge { display: inline-block; padding: 4px 10px; border-radius: 3px; font-size: 12px; font-weight: 500; }
  59. .status-success { background: #c8e6c9; color: #2e7d32; }
  60. .status-error { background: #ffcdd2; color: #c62828; }
  61. .status-warn { background: #fff3e0; color: #e65100; }
  62. .status-pending { background: #fff3e0; color: #e65100; }
  63. .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; }
  64. .field-desc-box { margin-top: 8px; background: #f5f7fa; border: 1px solid #e0e0e0; border-radius: 4px; padding: 10px; }
  65. .field-desc-title { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 6px; }
  66. .field-desc-table { width: 100%; border-collapse: collapse; font-size: 12px; }
  67. .field-desc-table th { background: #e8eaed; padding: 4px 8px; text-align: left; color: #333; font-weight: 600; border: 1px solid #ddd; }
  68. .field-desc-table td { padding: 4px 8px; border: 1px solid #ddd; color: #555; }
  69. .field-desc-table td code { background: #e8e8e8; padding: 1px 4px; border-radius: 2px; font-size: 11px; }
  70. .field-desc-details { margin-bottom: 12px; background: #f5f7fa; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px 12px; }
  71. .field-desc-summary { cursor: pointer; font-size: 13px; font-weight: 600; color: #555; user-select: none; padding: 2px 0; }
  72. .field-desc-summary:hover { color: #1976d2; }
  73. .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; }
  74. .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; }
  75. .summary-item { display: flex; align-items: center; gap: 6px; font-size: 14px; }
  76. .summary-count { font-weight: 700; font-size: 20px; }
  77. .summary-count.pass { color: #2e7d32; }
  78. .summary-count.fail { color: #c62828; }
  79. .summary-count.pending { color: #e65100; }
  80. .batch-buttons { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
  81. .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); }
  82. .side-btn { flex: 1; padding: 16px 24px; border: none; cursor: pointer; font-size: 16px; font-weight: 600; transition: all 0.25s; text-align: center; }
  83. .side-btn.user-side { background: #e3f2fd; color: #1565c0; }
  84. .side-btn.user-side.active { background: #1565c0; color: white; }
  85. .side-btn.admin-side { background: #fff3e0; color: #e65100; }
  86. .side-btn.admin-side.active { background: #e65100; color: white; }
  87. .side-btn:hover { opacity: 0.9; transform: translateY(-1px); }
  88. .admin-login-area { background: #fff8e1; border: 1px solid #ffe0b2; border-radius: 6px; }
  89. .admin-login-area .login-title { color: #e65100 !important; }
  90. .admin-tabs .module-tab.active { color: #e65100; border-bottom-color: #e65100; }
  91. .admin-tabs .module-tab:hover { background: #fff3e0; color: #e65100; }
  92. @media (max-width: 768px) {
  93. .form-grid { grid-template-columns: 1fr; }
  94. .toolbar { flex-direction: column; align-items: stretch; }
  95. .module-tab { font-size: 11px; padding: 8px 12px; }
  96. }
  97. </style>
  98. </head>
  99. <body>
  100. <div class="container">
  101. <h1>智价云(药店版) - 活动模块API验收测试工具</h1>
  102. <h2>覆盖签到有礼(周期制) + 邀请有礼 + 比价抽奖 用户端 &amp; 运营端 全量API | 独立登录管理 | 开发环境验证码固定 123456</h2>
  103. <!-- 用户侧/运营侧 切换器 -->
  104. <div class="side-switcher">
  105. <button class="side-btn user-side active" onclick="switchSide('user')">用户侧 (C端)</button>
  106. <button class="side-btn admin-side" onclick="switchSide('admin')">运营侧 (管理端)</button>
  107. </div>
  108. <!-- 全局工具栏 -->
  109. <div class="toolbar">
  110. <label>API 基础地址</label>
  111. <label for="baseUrl"></label><input type="text" id="baseUrl" value="http://localhost:8002" style="flex:1;">
  112. </div>
  113. <!-- 用户侧登录区 -->
  114. <div id="userLoginSection">
  115. <div class="toolbar">
  116. <label>用户Token</label>
  117. <label for="token"></label><input type="text" id="token" placeholder="登录后自动填入" style="flex:1;" readonly>
  118. <span id="roleBadge" class="role-tag" style="display:none;"></span>
  119. <button class="btn btn-outline btn-sm" onclick="copyToken()">复制Token</button>
  120. </div>
  121. <div class="toolbar" style="background:#f8f9fa;">
  122. <div class="login-section">
  123. <span class="login-title">短信登录</span>
  124. <label>手机号</label>
  125. <label for="phone"></label><input type="text" id="phone" value="18776156917" style="width:140px;">
  126. <label>验证码</label>
  127. <label for="smsCode"></label><input type="text" id="smsCode" placeholder="123456" value="123456" style="width:100px;">
  128. <button class="btn btn-primary btn-sm" onclick="sendSms()">发送验证码</button>
  129. <button class="btn btn-success btn-sm" onclick="smsLogin()">短信登录</button>
  130. </div>
  131. <div class="login-section">
  132. <span class="login-title">密码登录</span>
  133. <label>手机号/用户名</label>
  134. <label for="pwdAccount"></label><input type="text" id="pwdAccount" value="18776156917" placeholder="手机号或用户名" style="width:140px;">
  135. <label>密码</label>
  136. <label for="pwdPassword"></label><input type="password" id="pwdPassword" placeholder="输入密码" style="width:120px;">
  137. <button class="btn btn-success btn-sm" onclick="passwordLogin()">密码登录</button>
  138. </div>
  139. </div>
  140. </div>
  141. <!-- 运营侧登录区 -->
  142. <div id="adminLoginSection" style="display:none;">
  143. <div class="toolbar">
  144. <label>管理员Token</label>
  145. <label for="adminToken"></label><input type="text" id="adminToken" placeholder="管理员登录后自动填入" style="flex:1;" readonly>
  146. <span id="adminRoleBadge" class="role-tag" style="display:none;"></span>
  147. <button class="btn btn-outline btn-sm" onclick="copyToken()">复制Token</button>
  148. </div>
  149. <div class="toolbar admin-login-area">
  150. <div class="login-section">
  151. <span class="login-title">管理员登录(t_admin表)</span>
  152. <label>用户名</label>
  153. <label for="adminPwdUsername"></label><input type="text" id="adminPwdUsername" value="admin" placeholder="管理员用户名" style="width:150px;">
  154. <label>密码</label>
  155. <label for="adminPwdPassword"></label><input type="password" id="adminPwdPassword" value="admin@123" placeholder="管理员密码" style="width:140px;">
  156. <button class="btn btn-warning btn-sm" onclick="adminPasswordLogin()">管理员登录</button>
  157. </div>
  158. </div>
  159. </div>
  160. <!-- 测试统计栏 -->
  161. <div class="summary-bar">
  162. <span style="font-weight:600;">本页测试统计:</span>
  163. <div class="summary-item"><span style="font-weight:700;font-size:20px;color:#1565c0;" id="totalCount">0</span><span>总计</span></div>
  164. <div class="summary-item"><span class="summary-count pass" id="passCount">0</span><span>通过</span></div>
  165. <div class="summary-item"><span class="summary-count fail" id="failCount">0</span><span>失败</span></div>
  166. <div class="summary-item"><span class="summary-count pending" id="pendingCount">0</span><span>待测</span></div>
  167. <button class="btn btn-warning btn-sm" onclick="runAllTests()" style="margin-left:auto;">一键全部测试</button>
  168. <button class="btn btn-outline btn-sm" onclick="clearAllResults()">清除结果</button>
  169. </div>
  170. <!-- 用户侧模块标签栏 -->
  171. <div class="module-tabs" id="moduleTabs">
  172. <div class="module-tab active" data-module="checkin">1.签到有礼</div>
  173. <div class="module-tab" data-module="activity-invite">2.邀请有礼</div>
  174. <div class="module-tab" data-module="activity-checkin">3.签到送会员</div>
  175. <div class="module-tab" data-module="activity-lottery">4.比价抽奖</div>
  176. </div>
  177. <!-- 运营侧模块标签栏 -->
  178. <div class="module-tabs admin-tabs" id="adminModuleTabs" style="display:none;">
  179. <div class="module-tab active" data-module="admin-checkin">1.签到管理</div>
  180. <div class="module-tab" data-module="admin-activity-invite">2.邀请有礼管理</div>
  181. <div class="module-tab" data-module="admin-activity-checkin">3.签到送会员管理</div>
  182. <div class="module-tab" data-module="admin-activity-lottery">4.比价抽奖管理</div>
  183. </div>
  184. <div id="panels"></div>
  185. </div>
  186. <script>
  187. // ===================== 全局状态 =====================
  188. let activeSide = 'user';
  189. const BASE_URL = () => document.getElementById('baseUrl').value;
  190. const TOKEN = () => activeSide === 'admin'
  191. ? (document.getElementById('adminToken')?.value || '')
  192. : (document.getElementById('token')?.value || '');
  193. const PHONE = () => document.getElementById('phone')?.value || '';
  194. const SMS_CODE = () => document.getElementById('smsCode')?.value || '123456';
  195. let stats = { pass: 0, fail: 0, pending: 0, total: 0 };
  196. const USER_MODULES = ['checkin', 'activity-invite', 'activity-checkin', 'activity-lottery'];
  197. const ADMIN_MODULES = ['admin-checkin', 'admin-activity-invite', 'admin-activity-checkin', 'admin-activity-lottery'];
  198. // ===================== 大整数安全JSON解析 =====================
  199. function safeJsonParse(text) {
  200. if (!text || text.trim() === '' || text.trim() === 'null') return null;
  201. const safeText = text.replace(/([:,\[]\s*)(\d{16,})(\s*[,\]\}])/g, '$1"$2"$3');
  202. return JSON.parse(safeText);
  203. }
  204. // ===================== HTTP 请求 =====================
  205. async function api(method, path, body, requireAuth = true) {
  206. const headers = { 'Content-Type': 'application/json' };
  207. if (requireAuth && TOKEN()) { headers['Authorization'] = 'Bearer ' + TOKEN(); }
  208. const opts = { method, headers };
  209. if (body && method !== 'GET') { opts.body = JSON.stringify(body); }
  210. const startTime = Date.now();
  211. try {
  212. const res = await fetch(BASE_URL() + path, opts);
  213. const text = await res.text();
  214. let data;
  215. try { data = safeJsonParse(text); } catch (e) { data = { _raw: text }; }
  216. return { ok: res.ok, status: res.status, data, duration: Date.now() - startTime };
  217. } catch (e) {
  218. return { ok: false, status: 0, data: { error: e.message }, duration: Date.now() - startTime };
  219. }
  220. }
  221. // ===================== 测试执行 =====================
  222. async function runTest(sectionId, method, path, body, requireAuth, validatorName) {
  223. const box = document.getElementById(sectionId + '-box');
  224. const statusEl = document.getElementById(sectionId + '-status');
  225. const rawEl = document.getElementById(sectionId + '-raw');
  226. const durEl = document.getElementById(sectionId + '-duration');
  227. updateBadge(statusEl, 'pending', '运行中...');
  228. if (box) box.style.display = 'block';
  229. let resolvedPath = path;
  230. document.querySelectorAll(`[id^="${sectionId}-"]`).forEach(input => {
  231. if (input.tagName === 'INPUT') {
  232. const fieldName = input.id.replace(sectionId + '-', '');
  233. resolvedPath = resolvedPath.replace('{' + fieldName + '}', input.value || fieldName);
  234. if (input.value) {
  235. const queryVal = encodeURIComponent(input.value);
  236. resolvedPath = resolvedPath.replace(
  237. new RegExp('([?&])' + fieldName + '=[^&]*', 'g'),
  238. '$1' + fieldName + '=' + queryVal
  239. );
  240. }
  241. }
  242. });
  243. // 统计total(首次执行时计入)
  244. if (!stats.hasOwnProperty(sectionId)) {
  245. stats.total++;
  246. stats.pending = Math.max(0, stats.total - stats.pass - stats.fail);
  247. document.getElementById('totalCount').textContent = stats.total;
  248. document.getElementById('pendingCount').textContent = stats.pending;
  249. }
  250. const result = await api(method, resolvedPath, body, requireAuth);
  251. if (durEl) durEl.textContent = result.duration + 'ms';
  252. if (rawEl) rawEl.textContent = JSON.stringify(result.data, null, 2);
  253. let passed = false;
  254. if (validatorName && typeof window[validatorName] === 'function') {
  255. try { passed = window[validatorName](result); } catch (e) { passed = false; }
  256. } else {
  257. passed = result.ok && result.status === 200;
  258. }
  259. updateStats(sectionId, passed);
  260. updateBadge(statusEl, passed ? 'success' : 'error', passed ? '通过 ' + result.status : '失败 ' + result.status);
  261. if (box) box.style.display = 'block';
  262. }
  263. function updateBadge(el, cls, text) {
  264. if (!el) return;
  265. el.textContent = text;
  266. el.className = 'status-badge status-' + cls;
  267. }
  268. function updateStats(id, passed) {
  269. const old = stats[id];
  270. if (old === 'pass') stats.pass--;
  271. if (old === 'fail') stats.fail--;
  272. stats[id] = passed ? 'pass' : 'fail';
  273. if (passed) stats.pass++; else stats.fail++;
  274. stats.pending = Math.max(0, stats.total - stats.pass - stats.fail);
  275. document.getElementById('passCount').textContent = stats.pass;
  276. document.getElementById('failCount').textContent = stats.fail;
  277. document.getElementById('pendingCount').textContent = stats.pending;
  278. }
  279. // ===================== 登录相关 =====================
  280. async function sendSms() {
  281. const res = await api('POST', '/api/auth/sms/send', { phone: PHONE(), scene: 'login' }, false);
  282. if (res.ok) {
  283. document.getElementById('smsCode').value = '123456';
  284. alert('验证码已写入(开发测试模式,固定验证码: 123456)');
  285. } else {
  286. alert('发送失败: ' + JSON.stringify(res.data));
  287. }
  288. }
  289. async function smsLogin() {
  290. const phone = PHONE();
  291. const code = SMS_CODE();
  292. if (!phone) { alert('请填写手机号'); return; }
  293. const payload = { phone, code, scene: 'login' };
  294. const res = await api('POST', '/api/auth/sms/login', payload, false);
  295. if (res.data?.code === 200 && res.data?.data?.accessToken) {
  296. document.getElementById('token').value = res.data.data.accessToken;
  297. updateRoleBadge(res.data.data.role);
  298. alert('短信登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'USER'));
  299. } else {
  300. alert('登录失败: ' + (res.data?.message || '未知错误'));
  301. }
  302. }
  303. async function passwordLogin() {
  304. const account = document.getElementById('pwdAccount').value.trim();
  305. const password = document.getElementById('pwdPassword').value;
  306. if (!account || !password) { alert('请填写手机号/用户名和密码'); return; }
  307. let payload;
  308. if (/^\d{11}$/.test(account)) {
  309. payload = { phone: account, password };
  310. } else {
  311. payload = { username: account, password };
  312. }
  313. const res = await api('POST', '/api/auth/password/login', payload, false);
  314. if (res.data?.code === 200 && res.data?.data?.accessToken) {
  315. document.getElementById('token').value = res.data.data.accessToken;
  316. updateRoleBadge(res.data.data.role);
  317. alert('密码登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'USER'));
  318. } else {
  319. alert('登录失败: ' + (res.data?.message || '未知错误'));
  320. }
  321. }
  322. async function adminPasswordLogin() {
  323. const username = document.getElementById('adminPwdUsername').value;
  324. const password = document.getElementById('adminPwdPassword').value;
  325. if (!username || !password) { alert('请填写管理员用户名和密码'); return; }
  326. const payload = { username, password };
  327. const res = await api('POST', '/api/auth/password/login', payload, false);
  328. if (res.data?.code === 200 && res.data?.data?.accessToken) {
  329. document.getElementById('adminToken').value = res.data.data.accessToken;
  330. updateAdminRoleBadge(res.data.data.role);
  331. alert('管理员登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'ADMIN'));
  332. } else {
  333. alert('登录失败: ' + (res.data?.message || '未知错误'));
  334. }
  335. }
  336. function updateRoleBadge(role) {
  337. const badge = document.getElementById('roleBadge');
  338. if (badge) {
  339. if (role) { badge.textContent = role; badge.style.display = 'inline-block'; }
  340. else { badge.style.display = 'none'; }
  341. }
  342. }
  343. function updateAdminRoleBadge(role) {
  344. const badge = document.getElementById('adminRoleBadge');
  345. if (badge) {
  346. if (role) { badge.textContent = role; badge.style.display = 'inline-block'; }
  347. else { badge.style.display = 'none'; }
  348. }
  349. }
  350. function copyToken() {
  351. const token = TOKEN();
  352. if (!token) { alert('当前无Token'); return; }
  353. navigator.clipboard.writeText(token).then(() => alert('Token已复制到剪贴板'));
  354. }
  355. // ===================== 侧边切换 =====================
  356. function switchSide(side) {
  357. activeSide = side;
  358. const isAdmin = side === 'admin';
  359. document.getElementById('userLoginSection').style.display = isAdmin ? 'none' : '';
  360. document.getElementById('adminLoginSection').style.display = isAdmin ? '' : 'none';
  361. document.getElementById('moduleTabs').style.display = isAdmin ? 'none' : '';
  362. document.getElementById('adminModuleTabs').style.display = isAdmin ? '' : 'none';
  363. if (isAdmin) {
  364. const activeTab = document.querySelector('#adminModuleTabs .module-tab.active');
  365. renderModule(activeTab ? activeTab.dataset.module : 'admin-checkin');
  366. } else {
  367. const activeTab = document.querySelector('#moduleTabs .module-tab.active');
  368. renderModule(activeTab ? activeTab.dataset.module : 'checkin');
  369. }
  370. }
  371. document.getElementById('moduleTabs').addEventListener('click', (e) => {
  372. if (!e.target.classList.contains('module-tab')) return;
  373. document.querySelectorAll('#moduleTabs .module-tab').forEach(t => t.classList.remove('active'));
  374. e.target.classList.add('active');
  375. renderModule(e.target.dataset.module);
  376. });
  377. document.getElementById('adminModuleTabs').addEventListener('click', (e) => {
  378. if (!e.target.classList.contains('module-tab')) return;
  379. document.querySelectorAll('#adminModuleTabs .module-tab').forEach(t => t.classList.remove('active'));
  380. e.target.classList.add('active');
  381. renderModule(e.target.dataset.module);
  382. });
  383. // ===================== 渲染模块面板 =====================
  384. function renderModule(module) {
  385. const panels = document.getElementById('panels');
  386. let html = '';
  387. switch (module) {
  388. case 'checkin': html = renderCheckinModule(); break;
  389. case 'activity-invite': html = renderActivityInviteModule(); break;
  390. case 'activity-checkin': html = renderActivityCheckinModule(); break;
  391. case 'activity-lottery': html = renderActivityLotteryModule(); break;
  392. case 'admin-checkin': html = renderAdminCheckinModule(); break;
  393. case 'admin-activity-invite': html = renderAdminActivityInviteModule(); break;
  394. case 'admin-activity-checkin': html = renderAdminActivityCheckinModule(); break;
  395. case 'admin-activity-lottery': html = renderAdminActivityLotteryModule(); break;
  396. }
  397. panels.innerHTML = html;
  398. panels.querySelectorAll('input[data-raw]').forEach(input => {
  399. input.value = input.getAttribute('data-raw');
  400. input.removeAttribute('data-raw');
  401. });
  402. }
  403. function toValidatorName(id) {
  404. const parts = id.split('-');
  405. let camel = parts[0];
  406. for (let i = 1; i < parts.length; i++) {
  407. camel += parts[i].charAt(0).toUpperCase() + parts[i].slice(1);
  408. }
  409. return 'validate_' + camel;
  410. }
  411. // ==================== 字段说明数据 ====================
  412. const FIELD_DESC = {
  413. 'resultWrapper': [
  414. ['code', 'Integer', '业务状态码(200=成功)'],
  415. ['message', 'String', '提示信息(如:操作成功)'],
  416. ['data', 'Object/Array/null', '返回数据(详见下方字段)'],
  417. ['timestamp', 'Long', '响应时间戳(毫秒级)']
  418. ],
  419. 'checkinStatus': [
  420. ['data.checkedToday', 'Boolean', '今日是否已签到'],
  421. ['data.continuousDays', 'Integer', '当前连续签到天数'],
  422. ['data.nextCheckinDate', 'Date', '下一次可签到日期'],
  423. ['data.enabled', 'Boolean', '签到功能是否启用'],
  424. ['data.currentPeriodId', 'Long', '当前周期ID(无周期时为null)'],
  425. ['data.cycleMode', 'String', '周期模式(FRI_THU/MON_SUN)'],
  426. ['data.periodStartDate', 'Date', '当前周期起始日'],
  427. ['data.periodEndDate', 'Date', '当前周期截止日'],
  428. ['data.periodRequiredDays', 'Integer', '周期需签到天数'],
  429. ['data.rewardMode', 'String', '达标模式:CUMULATIVE-累计/CONSECUTIVE-连续'],
  430. ['data.periodCheckinCount', 'Integer', '当前周期已签到天数'],
  431. ['data.periodReachedTarget', 'Boolean', '当前周期是否已达标'],
  432. ['data.periodRewardDays', 'Integer', '达标可获会员天数'],
  433. ['data.rewardedCount', 'Long', '累计已领取奖励次数'],
  434. ['data.maxRewardCount', 'Integer', '活动期间最多可领取次数'],
  435. ['data.remainingCount', 'Long', '剩余可领取次数']
  436. ],
  437. 'checkinResult': [
  438. ['data.id', 'Long', '签到记录ID'],
  439. ['data.checkinDate', 'Date', '签到日期'],
  440. ['data.continuousDays', 'Integer', '签到时连续天数'],
  441. ['data.rewardAmount', 'Integer', '本次奖励(统一0,周期达标后才发奖)'],
  442. ['data.rewardType', 'String', '奖励类型(MEMBERSHIP)'],
  443. ['data.createTime', 'DateTime', '签到时间']
  444. ],
  445. 'checkinCalendar': [
  446. ['data.month', 'String', '年-月(如2026-07)'],
  447. ['data.checkedDays', 'Integer', '当月已签到天数'],
  448. ['data.continuousDays', 'Integer', '当前连续签到天数'],
  449. ['data.checkedDates', 'Array<Date>', '已签到日期列表'],
  450. ['data.checkedToday', 'Boolean', '今日是否已签到']
  451. ],
  452. 'checkinConfig': [
  453. ['data.id', 'Long', '配置ID(固定1)'],
  454. ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
  455. ['data.startTime', 'DateTime', '活动开始时间'],
  456. ['data.endTime', 'DateTime', '活动结束时间'],
  457. ['data.rewardType', 'String', '奖励类型(MEMBERSHIP-会员天数)'],
  458. ['data.rewardLevel', 'String', '奖励会员等级(PRO/ULTRA)'],
  459. ['data.cycleMode', 'String', '周期模式默认值(FRI_THU/MON_SUN)'],
  460. ['data.requiredDays', 'Integer', '周期需签到天数默认值'],
  461. ['data.rewardMode', 'String', '达标模式默认值:CUMULATIVE-累计/CONSECUTIVE-连续'],
  462. ['data.rewardDays', 'Integer', '周期达标奖励天数默认值'],
  463. ['data.maxRewardCount', 'Integer', '活动期间每用户最多领取次数'],
  464. ['data.remark', 'String', '备注'],
  465. ['data.createTime', 'DateTime', '创建时间'],
  466. ['data.updateTime', 'DateTime', '更新时间']
  467. ],
  468. 'checkinPeriod': [
  469. ['data[].id', 'Long', '周期ID'],
  470. ['data[].periodName', 'String', '周期名称'],
  471. ['data[].cycleMode', 'String', '周期模式(FRI_THU/MON_SUN)'],
  472. ['data[].startDate', 'Date', '周期起始日'],
  473. ['data[].endDate', 'Date', '周期截止日'],
  474. ['data[].requiredDays', 'Integer', '需签到天数'],
  475. ['data[].rewardMode', 'String', '达标模式:CUMULATIVE/CONSECUTIVE'],
  476. ['data[].rewardDays', 'Integer', '奖励会员天数'],
  477. ['data[].rewardLevel', 'String', '奖励会员等级'],
  478. ['data[].status', 'String', 'PENDING-待开始/ACTIVE-进行中/CLOSED-已结束'],
  479. ['data[].sort', 'Integer', '排序']
  480. ],
  481. 'checkinStats': [
  482. ['data.todayCheckinCount', 'Long', '今日签到人数'],
  483. ['data.yesterdayCheckinCount', 'Long', '昨日签到人数'],
  484. ['data.last7DaysCount', 'Long', '近7天签到人数(去重)'],
  485. ['data.last30DaysCount', 'Long', '近30天签到人数(去重)'],
  486. ['data.totalCheckinCount', 'Long', '累计签到总次数'],
  487. ['data.totalCheckinUsers', 'Long', '累计签到用户数(去重)'],
  488. ['data.totalRewardAmount', 'Long', '累计发放会员天数总和'],
  489. ['data.statDate', 'Date', '统计日期'],
  490. ['data.enabled', 'Boolean', '签到功能是否启用']
  491. ],
  492. 'adminCheckinRecord': [
  493. ['data.records[].id', 'Long', '签到记录ID'],
  494. ['data.records[].userId', 'Long', '用户ID'],
  495. ['data.records[].phone', 'String', '用户手机号'],
  496. ['data.records[].nickname', 'String', '用户昵称'],
  497. ['data.records[].pharmacyName', 'String', '药店名称'],
  498. ['data.records[].checkinDate', 'Date', '签到日期'],
  499. ['data.records[].continuousDays', 'Integer', '签到时连续天数'],
  500. ['data.records[].rewardType', 'String', '奖励类型'],
  501. ['data.records[].rewardAmount', 'Integer', '本次奖励会员天数'],
  502. ['data.records[].clientSource', 'String', '签到端'],
  503. ['data.records[].createTime', 'DateTime', '签到时间'],
  504. ['data.total', 'Long', '总记录数'],
  505. ['data.current', 'Long', '当前页'],
  506. ['data.size', 'Long', '每页条数'],
  507. ['data.pages', 'Long', '总页数']
  508. ],
  509. 'activityInviteStatus': [
  510. ['data.enabled', 'Boolean', '活动是否启用'],
  511. ['data.inWindow', 'Boolean', '是否在活动时间窗内'],
  512. ['data.rewardedCount', 'Long', '已获奖励次数'],
  513. ['data.maxInviterReward', 'Integer', '邀请人最多奖励次数'],
  514. ['data.remainingCount', 'Long', '剩余可获奖励次数']
  515. ],
  516. 'activityCheckinStatus': [
  517. ['data.enabled', 'Boolean', '活动是否启用'],
  518. ['data.inWindow', 'Boolean', '是否在活动时间窗内'],
  519. ['data.currentCycleStart', 'Date', '当前周期起始日(周五)'],
  520. ['data.currentCycleEnd', 'Date', '当前周期截止日(下周四)'],
  521. ['data.currentCycleCheckinCount', 'Integer', '当前周期内已签到天数'],
  522. ['data.requiredDays', 'Integer', '达标所需签到天数'],
  523. ['data.reachedTarget', 'Boolean', '当前周期是否已达标'],
  524. ['data.rewardDays', 'Integer', '达标后可获会员天数'],
  525. ['data.rewardedCount', 'Long', '已领取奖励次数'],
  526. ['data.maxRewardCount', 'Integer', '最多可领取奖励次数'],
  527. ['data.remainingCount', 'Long', '剩余可领取奖励次数']
  528. ],
  529. 'activityLotteryStatus': [
  530. ['data.enabled', 'Boolean', '活动是否启用'],
  531. ['data.inWindow', 'Boolean', '是否在活动时间窗内'],
  532. ['data.eligible', 'Boolean', '是否满足参与资格(会员等级)'],
  533. ['data.membershipLevel', 'String', '当前用户会员等级'],
  534. ['data.requireMembershipLevel', 'String', '所需会员等级'],
  535. ['data.availableChances', 'Integer', '今日可用抽奖机会数'],
  536. ['data.usedChances', 'Integer', '今日已用抽奖机会数']
  537. ],
  538. 'activityLotteryPrizes': [
  539. ['data[].id', 'Long', '奖品ID'],
  540. ['data[].name', 'String', '奖品名称'],
  541. ['data[].rewardType', 'String', '奖励类型(MEMBERSHIP会员/CRAWLER爬虫次数/EMPTY谢谢参与)'],
  542. ['data[].rewardDays', 'Integer', '会员奖励天数'],
  543. ['data[].rewardLevel', 'String', '会员奖励等级(PRO/ULTRA)'],
  544. ['data[].rewardAmount', 'Integer', '爬虫奖励次数(CRAWLER类型时有效,暂未实现)'],
  545. ['data[].sort', 'Integer', '排序']
  546. ],
  547. 'activityLotteryDraw': [
  548. ['data.recordId', 'Long', '抽奖记录ID'],
  549. ['data.prizeName', 'String', '奖品名称'],
  550. ['data.rewardType', 'String', '奖励类型(MEMBERSHIP会员/CRAWLER爬虫次数/EMPTY谢谢参与)'],
  551. ['data.rewardAmount', 'Integer', '奖励数量(预留)'],
  552. ['data.rewardDays', 'Integer', '会员奖励天数'],
  553. ['data.rewardLevel', 'String', '会员奖励等级(PRO/ULTRA)'],
  554. ['data.win', 'Boolean', '是否中奖(EMPTY=未中奖)'],
  555. ['data.createTime', 'DateTime', '抽奖时间']
  556. ],
  557. 'adminActivityInviteConfig': [
  558. ['data.id', 'Long', '配置ID(固定1)'],
  559. ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
  560. ['data.startTime', 'DateTime', '活动开始时间'],
  561. ['data.endTime', 'DateTime', '活动结束时间'],
  562. ['data.inviterRewardDays', 'Integer', '邀请人奖励天数'],
  563. ['data.inviteeRewardDays', 'Integer', '被邀请人奖励天数'],
  564. ['data.maxInviterReward', 'Integer', '邀请人最多奖励次数'],
  565. ['data.rewardTrigger', 'String', '奖励触发模式(REGISTRATION/LICENSE_APPROVED)'],
  566. ['data.requireMiniappInviter', 'Integer', '是否要求邀请人为小程序用户:0-否 1-是'],
  567. ['data.remark', 'String', '备注'],
  568. ['data.createTime', 'DateTime', '创建时间'],
  569. ['data.updateTime', 'DateTime', '更新时间']
  570. ],
  571. 'adminActivityInviteRevoke': [
  572. ['data', 'Integer', '撤销的奖励记录数']
  573. ],
  574. 'adminActivityCheckinConfig': [
  575. ['data.id', 'Long', '配置ID(固定1)'],
  576. ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
  577. ['data.startTime', 'DateTime', '活动开始时间'],
  578. ['data.endTime', 'DateTime', '活动结束时间'],
  579. ['data.cycleMode', 'String', '周期模式(FRI_THU=周五至下周四)'],
  580. ['data.requiredDays', 'Integer', '达标所需签到天数'],
  581. ['data.rewardDays', 'Integer', '达标奖励会员天数'],
  582. ['data.maxRewardCount', 'Integer', '每人最多可领取奖励次数'],
  583. ['data.remark', 'String', '备注'],
  584. ['data.createTime', 'DateTime', '创建时间'],
  585. ['data.updateTime', 'DateTime', '更新时间']
  586. ],
  587. 'adminActivityLotteryConfig': [
  588. ['data.id', 'Long', '配置ID(固定1)'],
  589. ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
  590. ['data.startTime', 'DateTime', '活动开始时间'],
  591. ['data.endTime', 'DateTime', '活动结束时间'],
  592. ['data.dailyMaxChance', 'Integer', '每个自然日最多获得抽奖机会数'],
  593. ['data.requireMembershipLevel', 'String', '参与所需会员等级(PRO/ULTRA/PLUS)'],
  594. ['data.remark', 'String', '备注'],
  595. ['data.createTime', 'DateTime', '创建时间'],
  596. ['data.updateTime', 'DateTime', '更新时间']
  597. ],
  598. 'adminActivityLotteryPrize': [
  599. ['data.id', 'Long', '奖品ID'],
  600. ['data.name', 'String', '奖品名称'],
  601. ['data.rewardType', 'String', '奖励类型(MEMBERSHIP会员/CRAWLER爬虫次数/EMPTY谢谢参与)'],
  602. ['data.rewardAmount', 'Integer', '奖励数量(预留)'],
  603. ['data.rewardDays', 'Integer', '会员奖励天数'],
  604. ['data.rewardLevel', 'String', '会员奖励等级'],
  605. ['data.probabilityWeight', 'Integer', '中奖概率权重(越大越容易中)'],
  606. ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
  607. ['data.sort', 'Integer', '排序(数字越小越靠前)']
  608. ]
  609. };
  610. function renderFieldDesc(descKey) {
  611. const fields = FIELD_DESC[descKey];
  612. if (!fields) return '';
  613. let rows = fields.map(f => `<tr><td><code>${f[0]}</code></td><td>${f[1]}</td><td>${f[2]}</td></tr>`).join('');
  614. return `<details open class="field-desc-details">
  615. <summary class="field-desc-summary">📋 返回字段说明</summary>
  616. <table class="field-desc-table"><tr><th>字段名</th><th>类型</th><th>说明</th></tr>${rows}</table>
  617. </details>`;
  618. }
  619. // ==================== 调用示例生成 ====================
  620. function renderCurlExample(id, method, path, fields) {
  621. const methodUpper = method.toUpperCase();
  622. const baseUrl = document.getElementById('baseUrl').value;
  623. let curl = `curl -X ${methodUpper} "${baseUrl}${path}"`;
  624. curl += ` \\\n -H "Authorization: Bearer \${TOKEN}"`;
  625. if (methodUpper === 'POST' || methodUpper === 'PUT') {
  626. curl += ` \\\n -H "Content-Type: application/json"`;
  627. }
  628. if (fields && fields.length > 0) {
  629. let bodyParts = [];
  630. fields.forEach(f => {
  631. let val = f.default || f.placeholder || '';
  632. bodyParts.push(`"${f.name}": "${val}"`);
  633. });
  634. if (bodyParts.length > 0) {
  635. curl += ` \\\n -d '{${bodyParts.join(', ')}}'`;
  636. }
  637. }
  638. return `<details class="field-desc-details">
  639. <summary class="field-desc-summary">📞 cURL 调用示例</summary>
  640. <pre class="curl-example">${curl}</pre>
  641. </details>`;
  642. }
  643. // ==================== 测试用例区块渲染 ====================
  644. function testSection(id, title, method, methodClass, path, authTag, authClass, fields, hasValidator, descKey, desc) {
  645. const authLabel = authTag === '需认证' ? '需认证' : '公开';
  646. const ac = authTag === '需认证' ? 'auth-required' : 'auth-public';
  647. const side = path.includes('/api/admin') ? 'admin' : 'user';
  648. const sideClass = side === 'admin' ? 'side-admin' : 'side-user';
  649. const sideLabel = side === 'admin' ? '运营侧' : '用户侧';
  650. let fieldsHtml = '';
  651. if (fields) {
  652. fieldsHtml = '<div class="form-grid">';
  653. fields.forEach(f => {
  654. const escapedDefault = (f.default || '').replace(/"/g, '&quot;');
  655. const escapedPlaceholder = (f.placeholder || '').replace(/"/g, '&quot;');
  656. const hasRaw = f.default && f.default.includes('"');
  657. 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>`;
  658. });
  659. fieldsHtml += '</div>';
  660. }
  661. const descHtml = desc ? `<div class="section-desc">${desc}</div>` : '';
  662. const fieldDescHtml = descKey ? renderFieldDesc(descKey) : '';
  663. const curlHtml = renderCurlExample(id, method, path, fields);
  664. return `
  665. <div class="test-section">
  666. <div class="section-title">
  667. <span class="method method-${methodClass}">${method}</span>
  668. <span class="path">${path}</span>
  669. <span class="side-tag ${sideClass}">${sideLabel}</span>
  670. <span class="${ac} auth-tag">${authLabel}</span>
  671. </div>
  672. ${descHtml}
  673. ${curlHtml}
  674. ${fieldDescHtml}
  675. ${fieldsHtml}
  676. <button class="btn btn-primary" onclick="runTest('${id}', '${method}', '${path}', ${buildBody(id, fields)}, ${authTag === '公开' ? 'false' : 'true'}, ${hasValidator ? `'${toValidatorName(id)}'` : 'null'})">执行测试</button>
  677. <div class="response-box" id="${id}-box" style="display:none;">
  678. <div class="response-header">
  679. <span class="response-title">响应 <span id="${id}-duration" style="color:#999;font-weight:400;"></span></span>
  680. <span class="status-badge" id="${id}-status"></span>
  681. </div>
  682. <div class="response-content" id="${id}-raw"></div>
  683. </div>
  684. </div>`;
  685. }
  686. function buildBody(id, fields) {
  687. if (!fields || fields.length === 0) return 'null';
  688. let parts = [];
  689. fields.forEach(f => {
  690. const forceArray = f.type === 'array';
  691. const asJson = f.type === 'json';
  692. const forceString = f.type === 'string';
  693. if (asJson) {
  694. parts.push(`&quot;${f.name}&quot;: safeJsonParse(document.getElementById('${id}-${f.name}').value)`);
  695. } else if (forceString) {
  696. parts.push(`&quot;${f.name}&quot;: document.getElementById('${id}-${f.name}').value || null`);
  697. } else {
  698. parts.push(`&quot;${f.name}&quot;: parseFieldValue(document.getElementById('${id}-${f.name}').value, ${forceArray})`);
  699. }
  700. });
  701. return `{${parts.join(',')}}`;
  702. }
  703. function parseFieldValue(val, forceArray) {
  704. if (val === '' || val === undefined || val === null) return forceArray ? [] : null;
  705. if (val === 'true') return forceArray ? [true] : true;
  706. if (val === 'false') return forceArray ? [false] : false;
  707. if (val.includes(',')) {
  708. const items = val.split(',').map(v => v.trim()).filter(v => v !== '');
  709. return items.map(v => toNumberIfSafe(v));
  710. }
  711. if (/^\d+(\.\d+)?$/.test(val)) return forceArray ? [toNumberIfSafe(val)] : toNumberIfSafe(val);
  712. return forceArray ? [val] : val;
  713. }
  714. function toNumberIfSafe(v) {
  715. if (!/^\d+(\.\d+)?$/.test(v)) return v;
  716. const num = Number(v);
  717. if (Number.isInteger(num) && !Number.isSafeInteger(num)) return v;
  718. return num;
  719. }
  720. // ==================== 用户端:签到模块 ====================
  721. function renderCheckinModule() {
  722. return `
  723. <div class="batch-buttons">
  724. <button class="btn btn-warning btn-sm" onclick="runCheckinTests()">运行全部签到测试</button>
  725. </div>
  726. ${testSection('checkin-status', '今日签到状态+周期进度', 'GET', 'get', '/api/checkin/status', '需认证', 'auth-required', null, true, 'checkinStatus', '获取当前用户今日签到状态、连续天数、当前周期信息(起止日/需签到天数/达标模式/已签到天数/是否达标/可获奖励)、累计领取统计')}
  727. ${testSection('checkin-do', '执行签到', 'POST', 'post', '/api/checkin', '需认证', 'auth-required',
  728. [{label:'签到端(clientSource,可选)', name:'clientSource', placeholder:'WINDOWS/ANDROID/MINIAPP', default:'WINDOWS'}], true, 'checkinResult', '执行当日签到(仅记录,不立即发奖)。自动关联当前进行中周期,签到后触发上一周期达标奖励检查。每个自然日仅可签到一次')}
  729. ${testSection('checkin-calendar', '当月签到日历', 'GET', 'get', '/api/checkin/calendar?year=2026&month=7', '需认证', 'auth-required',
  730. [{label:'年份(year,可选)', name:'year', placeholder:'2026', default:'2026'},
  731. {label:'月份(month,可选)', name:'month', placeholder:'7', default:'7'}], true, 'checkinCalendar', '获取指定月份的签到日历,不传默认当月')}
  732. ${testSection('checkin-records', '我的签到记录', 'GET', 'get', '/api/checkin/records?limit=30', '需认证', 'auth-required',
  733. [{label:'条数(limit,可选)', name:'limit', placeholder:'30', default:'30'}], true, null, '获取当前用户的签到历史记录,按签到日期倒序')}`;
  734. }
  735. // ==================== 用户端:邀请有礼模块 ====================
  736. function renderActivityInviteModule() {
  737. return `
  738. <div class="batch-buttons">
  739. <button class="btn btn-warning btn-sm" onclick="runActivityInviteTests()">运行全部邀请有礼测试</button>
  740. </div>
  741. ${testSection('activity-invite-status', '邀请有礼活动状态', 'GET', 'get', '/api/activity/invite/status', '需认证', 'auth-required', null, true, 'activityInviteStatus', '查询当前邀请有礼活动状态:是否开启、奖励模式、已获/剩余次数')}`;
  742. }
  743. // ==================== 用户端:签到送会员模块(旧版独立接口,周期信息已整合到签到状态接口) ====================
  744. function renderActivityCheckinModule() {
  745. return `
  746. <div class="batch-buttons">
  747. <button class="btn btn-warning btn-sm" onclick="runActivityCheckinTests()">运行全部签到送会员测试</button>
  748. </div>
  749. ${testSection('activity-checkin-status', '签到送会员周期状态(旧版)', 'GET', 'get', '/api/activity/checkin-cycle/status', '需认证', 'auth-required', null, true, 'activityCheckinStatus', '【旧版】查询当前周期签到进度。新版签到状态已整合周期信息到 /api/checkin/status')}`;
  750. }
  751. // ==================== 用户端:比价抽奖模块 ====================
  752. function renderActivityLotteryModule() {
  753. return `
  754. <div class="batch-buttons">
  755. <button class="btn btn-warning btn-sm" onclick="runActivityLotteryTests()">运行全部比价抽奖测试</button>
  756. </div>
  757. ${testSection('activity-lottery-prizes', '奖励信息(可抽奖品列表)', 'GET', 'get', '/api/activity/lottery/prizes', '需认证', 'auth-required', null, true, 'activityLotteryPrizes', '查看当前可抽奖品列表(仅启用奖品,不显示概率权重等敏感信息)')}
  758. ${testSection('activity-lottery-status', '比价抽奖活动状态', 'GET', 'get', '/api/activity/lottery/status', '需认证', 'auth-required', null, true, 'activityLotteryStatus', '查询抽奖活动状态:是否开启、是否满足资格、今日可用机会')}
  759. ${testSection('activity-lottery-draw', '执行抽奖', 'POST', 'post', '/api/activity/lottery/draw', '需认证', 'auth-required', null, true, 'activityLotteryDraw', '消耗一次抽奖机会,随机抽取奖品(会员/爬虫次数/谢谢参与)')}
  760. ${testSection('activity-lottery-records', '我的抽奖记录', 'GET', 'get', '/api/activity/lottery/records?limit=20', '需认证', 'auth-required',
  761. [{label:'条数(limit,可选)', name:'limit', placeholder:'20', default:'20'}], true, null, '获取当前用户的抽奖历史记录')}`;
  762. }
  763. // ==================== 运营端:签到管理模块 ====================
  764. function renderAdminCheckinModule() {
  765. return `
  766. <div class="batch-buttons">
  767. <button class="btn btn-warning btn-sm" onclick="runAdminCheckinTests()">运行全部签到管理测试</button>
  768. </div>
  769. ${testSection('admin-checkin-config', '签到全局配置查询', 'GET', 'get', '/api/admin/checkin/config', '需认证', 'auth-required', null, true, 'checkinConfig', '获取签到全局配置:活动时间窗、周期默认值、奖励规则')}
  770. ${testSection('admin-checkin-config-update', '更新签到全局配置', 'PUT', 'put', '/api/admin/checkin/config', '需认证', 'auth-required',
  771. [
  772. {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
  773. {label:'开始时间(startTime)', name:'startTime', placeholder:'2026-07-10 00:00:00', default:'2026-07-10 00:00:00'},
  774. {label:'结束时间(endTime)', name:'endTime', placeholder:'2026-07-31 23:59:59', default:'2026-07-31 23:59:59'},
  775. {label:'会员等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO/ULTRA', default:'PRO'},
  776. {label:'周期模式(cycleMode)', name:'cycleMode', placeholder:'FRI_THU/MON_SUN', default:'FRI_THU'},
  777. {label:'需签到天数(requiredDays)', name:'requiredDays', placeholder:'4', default:'4'},
  778. {label:'达标模式(rewardMode)', name:'rewardMode', placeholder:'CUMULATIVE/CONSECUTIVE', default:'CUMULATIVE'},
  779. {label:'奖励天数(rewardDays)', name:'rewardDays', placeholder:'7', default:'7'},
  780. {label:'最多领取次数(maxRewardCount)', name:'maxRewardCount', placeholder:'3', default:'3'},
  781. {label:'备注(remark)', name:'remark', placeholder:'签到送会员', default:'签到送会员:每周期累计签到达标后发放奖励'}
  782. ], true, 'checkinConfig', '更新签到全局配置。rewardMode: CUMULATIVE-周期内累计签到N天 / CONSECUTIVE-周期内连续签到N天')}
  783. ${testSection('admin-checkin-periods', '周期列表', 'GET', 'get', '/api/admin/checkin/periods', '需认证', 'auth-required', null, true, 'checkinPeriod', '获取所有签到周期,每个周期可独立配置起止日期、需签到天数、达标模式、奖励')}
  784. ${testSection('admin-checkin-period-create', '创建周期', 'POST', 'post', '/api/admin/checkin/periods', '需认证', 'auth-required',
  785. [
  786. {label:'周期名称(periodName)', name:'periodName', placeholder:'第N周', default:'测试周期'},
  787. {label:'周期模式(cycleMode)', name:'cycleMode', placeholder:'FRI_THU', default:'FRI_THU'},
  788. {label:'起始日(startDate)', name:'startDate', placeholder:'2026-07-10', default:'2026-07-10'},
  789. {label:'截止日(endDate)', name:'endDate', placeholder:'2026-07-16', default:'2026-07-16'},
  790. {label:'需签到天数(requiredDays)', name:'requiredDays', placeholder:'4', default:'4'},
  791. {label:'达标模式(rewardMode)', name:'rewardMode', placeholder:'CUMULATIVE/CONSECUTIVE', default:'CUMULATIVE'},
  792. {label:'奖励天数(rewardDays)', name:'rewardDays', placeholder:'7', default:'7'},
  793. {label:'奖励等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO', default:'PRO'},
  794. {label:'排序(sort)', name:'sort', placeholder:'0', default:'99'}
  795. ], true, 'checkinPeriod', '创建一个签到周期。rewardMode: CUMULATIVE累计/CONSECUTIVE连续')}
  796. ${testSection('admin-checkin-period-update', '更新周期', 'PUT', 'put', '/api/admin/checkin/periods/{id}', '需认证', 'auth-required',
  797. [
  798. {label:'周期ID(id)', name:'id', placeholder:'1', default:''},
  799. {label:'周期名称(periodName)', name:'periodName', placeholder:'第N周', default:''},
  800. {label:'周期模式(cycleMode)', name:'cycleMode', placeholder:'FRI_THU', default:''},
  801. {label:'起始日(startDate)', name:'startDate', placeholder:'2026-07-10', default:''},
  802. {label:'截止日(endDate)', name:'endDate', placeholder:'2026-07-16', default:''},
  803. {label:'需签到天数(requiredDays)', name:'requiredDays', placeholder:'4', default:''},
  804. {label:'达标模式(rewardMode)', name:'rewardMode', placeholder:'CUMULATIVE/CONSECUTIVE', default:''},
  805. {label:'奖励天数(rewardDays)', name:'rewardDays', placeholder:'7', default:''},
  806. {label:'奖励等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO', default:''},
  807. {label:'排序(sort)', name:'sort', placeholder:'0', default:''}
  808. ], true, 'checkinPeriod', '更新签到周期配置')}
  809. ${testSection('admin-checkin-period-delete', '删除周期', 'DELETE', 'delete', '/api/admin/checkin/periods/{id}', '需认证', 'auth-required',
  810. [{label:'周期ID(id)', name:'id', placeholder:'需删除的周期ID', default:''}], true, null, '删除指定签到周期')}
  811. ${testSection('admin-checkin-stats', '签到统计概览', 'GET', 'get', '/api/admin/checkin/stats', '需认证', 'auth-required', null, true, 'checkinStats', '签到数据统计:今日/昨日/近7天/近30天签到人数、累计签到次数与用户数、累计发放奖励')}
  812. ${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',
  813. [{label:'页码(page)', name:'page', placeholder:'1', default:'1'},
  814. {label:'每页条数(size)', name:'size', placeholder:'10', default:'10'},
  815. {label:'用户ID(userId,可选)', name:'userId', placeholder:'1', default:''},
  816. {label:'起始日期(startDate,可选)', name:'startDate', placeholder:'2026-07-01', default:''},
  817. {label:'截止日期(endDate,可选)', name:'endDate', placeholder:'2026-07-07', default:''},
  818. {label:'签到端(clientSource,可选)', name:'clientSource', placeholder:'WINDOWS', default:''}], true, 'adminCheckinRecord', '分页查询全局签到记录,关联用户手机号/昵称/药店名。条件均可选')}
  819. ${testSection('admin-checkin-user-records', '指定用户签到记录', 'GET', 'get', '/api/admin/checkin/user/{userId}/records', '需认证', 'auth-required',
  820. [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'}], true, null, '查询指定用户的全部签到记录(最多500条),含用户基础信息')}`;
  821. }
  822. // ==================== 运营端:邀请有礼管理模块 ====================
  823. function renderAdminActivityInviteModule() {
  824. return `
  825. <div class="batch-buttons">
  826. <button class="btn btn-warning btn-sm" onclick="runAdminActivityInviteTests()">运行全部邀请有礼管理测试</button>
  827. </div>
  828. ${testSection('admin-activity-invite-config', '邀请有礼配置查询', 'GET', 'get', '/api/admin/activity/invite/config', '需认证', 'auth-required', null, true, 'adminActivityInviteConfig', '获取当前邀请有礼活动配置:启用状态、时间窗、奖励天数、触发模式、上限')}
  829. ${testSection('admin-activity-invite-config-update', '更新邀请有礼配置', 'PUT', 'put', '/api/admin/activity/invite/config', '需认证', 'auth-required',
  830. [
  831. {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
  832. {label:'开始时间(startTime)', name:'startTime', placeholder:'2026-07-10 00:00:00', default:'2026-07-10 00:00:00'},
  833. {label:'结束时间(endTime)', name:'endTime', placeholder:'2026-07-31 23:59:59', default:'2026-07-31 23:59:59'},
  834. {label:'邀请人奖励天数(inviterRewardDays)', name:'inviterRewardDays', placeholder:'30', default:'30'},
  835. {label:'被邀请人奖励天数(inviteeRewardDays)', name:'inviteeRewardDays', placeholder:'30', default:'30'},
  836. {label:'邀请人最多奖励次数(maxInviterReward)', name:'maxInviterReward', placeholder:'30', default:'30'},
  837. {label:'奖励触发模式(rewardTrigger)', name:'rewardTrigger', placeholder:'REGISTRATION/LICENSE_APPROVED', default:'LICENSE_APPROVED'},
  838. {label:'要求小程序用户(requireMiniappInviter)', name:'requireMiniappInviter', placeholder:'0不要求/1要求', default:'1'},
  839. {label:'备注(remark)', name:'remark', placeholder:'邀请有礼活动', default:'邀请有礼:被邀请人资质审核通过后双方各得30天高级会员'}
  840. ], true, 'adminActivityInviteConfig', '更新邀请有礼活动规则。rewardTrigger=REGISTRATION注册即发,LICENSE_APPROVED资质审核通过后发')}
  841. ${testSection('admin-activity-invite-revoke', '撤销邀请奖励(反作弊)', 'POST', 'post', '/api/admin/activity/invite/revoke', '需认证', 'auth-required',
  842. [
  843. {label:'被邀请人ID(inviteeId)', name:'inviteeId', placeholder:'作弊者用户ID', default:''},
  844. {label:'撤销原因(reason)', name:'reason', placeholder:'冒用他人资质', default:'冒用他人资质'}
  845. ], true, 'adminActivityInviteRevoke', '反作弊:根据被邀请人ID撤销其对应的邀请奖励,标记为已撤销状态')}`;
  846. }
  847. // ==================== 运营端:签到送会员管理模块(旧版独立配置,已被签到全局配置+周期表替代,保留兼容) ====================
  848. function renderAdminActivityCheckinModule() {
  849. return `
  850. <div class="batch-buttons">
  851. <button class="btn btn-warning btn-sm" onclick="runAdminActivityCheckinTests()">运行全部签到送会员管理测试</button>
  852. </div>
  853. ${testSection('admin-activity-checkin-config', '签到送会员旧版配置查询', 'GET', 'get', '/api/admin/activity/checkin-cycle/config', '需认证', 'auth-required', null, true, 'adminActivityCheckinConfig', '【旧版】获取签到送会员活动独立配置。新版签到体系使用 /api/admin/checkin/config + 周期表管理')}
  854. ${testSection('admin-activity-checkin-config-update', '更新签到送会员旧版配置', 'PUT', 'put', '/api/admin/activity/checkin-cycle/config', '需认证', 'auth-required',
  855. [
  856. {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
  857. {label:'开始时间(startTime)', name:'startTime', placeholder:'2026-07-10 00:00:00', default:'2026-07-10 00:00:00'},
  858. {label:'结束时间(endTime)', name:'endTime', placeholder:'2026-07-31 23:59:59', default:'2026-07-31 23:59:59'},
  859. {label:'周期模式(cycleMode)', name:'cycleMode', placeholder:'FRI_THU', default:'FRI_THU'},
  860. {label:'需签到天数(requiredDays)', name:'requiredDays', placeholder:'4', default:'4'},
  861. {label:'奖励天数(rewardDays)', name:'rewardDays', placeholder:'7', default:'7'},
  862. {label:'最多领取次数(maxRewardCount)', name:'maxRewardCount', placeholder:'3', default:'3'},
  863. {label:'备注(remark)', name:'remark', placeholder:'签到送会员', default:'每周五至下周四为一周期,累计4天签到得7天高级会员,最多3次'}
  864. ], true, 'adminActivityCheckinConfig', '【旧版】更新签到送会员活动独立配置。新版请使用签到管理面板的全局配置+周期表')}`;
  865. }
  866. // ==================== 运营端:比价抽奖管理模块 ====================
  867. function renderAdminActivityLotteryModule() {
  868. return `
  869. <div class="batch-buttons">
  870. <button class="btn btn-warning btn-sm" onclick="runAdminActivityLotteryTests()">运行全部比价抽奖管理测试</button>
  871. </div>
  872. ${testSection('admin-activity-lottery-config', '比价抽奖配置查询', 'GET', 'get', '/api/admin/activity/lottery/config', '需认证', 'auth-required', null, true, 'adminActivityLotteryConfig', '获取当前比价抽奖活动配置:每日机会数、所需会员等级')}
  873. ${testSection('admin-activity-lottery-config-update', '更新比价抽奖配置', 'PUT', 'put', '/api/admin/activity/lottery/config', '需认证', 'auth-required',
  874. [
  875. {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
  876. {label:'开始时间(startTime)', name:'startTime', placeholder:'2026-07-10 00:00:00', default:'2026-07-10 00:00:00'},
  877. {label:'结束时间(endTime)', name:'endTime', placeholder:'2026-07-31 23:59:59', default:'2026-07-31 23:59:59'},
  878. {label:'每日最多机会(dailyMaxChance)', name:'dailyMaxChance', placeholder:'1', default:'1'},
  879. {label:'所需会员等级(requireMembershipLevel)', name:'requireMembershipLevel', placeholder:'PRO', default:'PRO'},
  880. {label:'备注(remark)', name:'remark', placeholder:'比价抽奖', default:'高级会员每自然日首次成功比价获得1次抽奖机会'}
  881. ], true, 'adminActivityLotteryConfig', '更新比价抽奖活动规则')}
  882. ${testSection('admin-activity-lottery-prizes', '奖品池列表', 'GET', 'get', '/api/admin/activity/lottery/prizes', '需认证', 'auth-required', null, true, null, '获取所有奖品及其权重配置')}
  883. ${testSection('admin-activity-lottery-prize-create', '新增奖品', 'POST', 'post', '/api/admin/activity/lottery/prizes', '需认证', 'auth-required',
  884. [
  885. {label:'奖品名称(name)', name:'name', placeholder:'高级会员3天', default:'高级会员3天'},
  886. {label:'奖励类型(rewardType)', name:'rewardType', placeholder:'MEMBERSHIP/CRAWLER/EMPTY', default:'MEMBERSHIP'},
  887. {label:'会员天数(rewardDays)', name:'rewardDays', placeholder:'3', default:'3'},
  888. {label:'会员等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO/ULTRA', default:'PRO'},
  889. {label:'爬虫次数(rewardAmount)', name:'rewardAmount', placeholder:'CRAWLER类型时填写', default:'0'},
  890. {label:'概率权重(probabilityWeight)', name:'probabilityWeight', placeholder:'权重越大越容易中', default:'20'},
  891. {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
  892. {label:'排序(sort)', name:'sort', placeholder:'0', default:'3'}
  893. ], true, 'adminActivityLotteryPrize', '新增奖品到奖品池。rewardType: MEMBERSHIP会员/CRAWLER爬虫次数(暂未实现)/EMPTY谢谢参与')}
  894. ${testSection('admin-activity-lottery-prize-update', '更新奖品', 'PUT', 'put', '/api/admin/activity/lottery/prizes/{id}', '需认证', 'auth-required',
  895. [
  896. {label:'奖品ID(id)', name:'id', placeholder:'1', default:''},
  897. {label:'奖品名称(name)', name:'name', placeholder:'高级会员3天', default:'高级会员3天'},
  898. {label:'奖励类型(rewardType)', name:'rewardType', placeholder:'MEMBERSHIP/CRAWLER/EMPTY', default:'MEMBERSHIP'},
  899. {label:'会员天数(rewardDays)', name:'rewardDays', placeholder:'3', default:'3'},
  900. {label:'会员等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO/ULTRA', default:'PRO'},
  901. {label:'爬虫次数(rewardAmount)', name:'rewardAmount', placeholder:'CRAWLER类型时填写', default:'0'},
  902. {label:'概率权重(probabilityWeight)', name:'probabilityWeight', placeholder:'权重越大越容易中', default:'20'},
  903. {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
  904. {label:'排序(sort)', name:'sort', placeholder:'0', default:'3'}
  905. ], true, 'adminActivityLotteryPrize', '更新奖品池中的奖品配置')}
  906. ${testSection('admin-activity-lottery-prize-delete', '删除奖品', 'DELETE', 'delete', '/api/admin/activity/lottery/prizes/{id}', '需认证', 'auth-required',
  907. [{label:'奖品ID(id)', name:'id', placeholder:'需删除的奖品ID', default:''}], true, null, '从奖品池中删除指定奖品')}`;
  908. }
  909. // ===================== 批量测试 ====================
  910. function getSectionsInModule(modulePrefix) {
  911. return Array.from(document.querySelectorAll('.test-section')).filter(s => {
  912. const btn = s.querySelector('button[onclick*="runTest"]');
  913. return btn && btn.getAttribute('onclick').includes("'" + modulePrefix + "-");
  914. });
  915. }
  916. function countTestsInCurrentPanel() {
  917. return document.querySelectorAll('.test-section button[onclick*="runTest"]').length;
  918. }
  919. async function runBatchTests(modulePrefix) {
  920. const sections = getSectionsInModule(modulePrefix);
  921. for (const section of sections) {
  922. const btn = section.querySelector('button[onclick*="runTest"]');
  923. if (btn) {
  924. const onclick = btn.getAttribute('onclick') || '';
  925. const match = onclick.match(/runTest\('([^']+)'/);
  926. if (match && !stats.hasOwnProperty(match[1])) {
  927. stats.total++;
  928. }
  929. }
  930. }
  931. stats.pending = Math.max(0, stats.total - stats.pass - stats.fail);
  932. document.getElementById('totalCount').textContent = stats.total;
  933. document.getElementById('pendingCount').textContent = stats.pending;
  934. for (const section of sections) {
  935. const btn = section.querySelector('button[onclick*="runTest"]');
  936. if (btn) {
  937. btn.click();
  938. await new Promise(r => setTimeout(r, 600));
  939. }
  940. }
  941. }
  942. async function runAllTests() {
  943. resetStats();
  944. for (const m of USER_MODULES) {
  945. const tab = document.querySelector(`#moduleTabs .module-tab[data-module="${m}"]`);
  946. if (tab) tab.click();
  947. await new Promise(r => setTimeout(r, 50));
  948. stats.total += countTestsInCurrentPanel();
  949. }
  950. for (const m of ADMIN_MODULES) {
  951. const tab = document.querySelector(`#adminModuleTabs .module-tab[data-module="${m}"]`);
  952. if (tab) tab.click();
  953. await new Promise(r => setTimeout(r, 50));
  954. stats.total += countTestsInCurrentPanel();
  955. }
  956. document.getElementById('totalCount').textContent = stats.total;
  957. stats.pending = stats.total;
  958. document.getElementById('pendingCount').textContent = stats.pending;
  959. switchSide('user');
  960. for (const m of USER_MODULES) {
  961. try {
  962. const tab = document.querySelector(`#moduleTabs .module-tab[data-module="${m}"]`);
  963. if (tab) tab.click();
  964. await new Promise(r => setTimeout(r, 100));
  965. await runBatchTests(m);
  966. } catch (e) { console.error('模块 ' + m + ' 测试出错:', e); }
  967. }
  968. switchSide('admin');
  969. for (const m of ADMIN_MODULES) {
  970. try {
  971. const tab = document.querySelector(`#adminModuleTabs .module-tab[data-module="${m}"]`);
  972. if (tab) tab.click();
  973. await new Promise(r => setTimeout(r, 100));
  974. await runBatchTests(m);
  975. } catch (e) { console.error('模块 ' + m + ' 测试出错:', e); }
  976. }
  977. }
  978. function resetStats() {
  979. const keys = Object.keys(stats);
  980. for (const k of keys) {
  981. if (k !== 'pass' && k !== 'fail' && k !== 'pending' && k !== 'total') {
  982. delete stats[k];
  983. }
  984. }
  985. stats.pass = 0;
  986. stats.fail = 0;
  987. stats.pending = 0;
  988. stats.total = 0;
  989. document.getElementById('totalCount').textContent = '0';
  990. document.getElementById('passCount').textContent = '0';
  991. document.getElementById('failCount').textContent = '0';
  992. document.getElementById('pendingCount').textContent = '0';
  993. }
  994. function clearAllResults() {
  995. document.querySelectorAll('.response-box').forEach(b => b.style.display = 'none');
  996. document.querySelectorAll('.status-badge').forEach(b => { b.textContent = ''; b.className = 'status-badge'; });
  997. resetStats();
  998. }
  999. // 用户侧/运营侧快捷批量运行
  1000. function runCheckinTests() { runBatchTests('checkin'); }
  1001. function runActivityInviteTests() { runBatchTests('activity-invite'); }
  1002. function runActivityCheckinTests() { runBatchTests('activity-checkin'); }
  1003. function runActivityLotteryTests() { runBatchTests('activity-lottery'); }
  1004. function runAdminCheckinTests() { runBatchTests('admin-checkin'); }
  1005. function runAdminActivityInviteTests() { runBatchTests('admin-activity-invite'); }
  1006. function runAdminActivityCheckinTests() { runBatchTests('admin-activity-checkin'); }
  1007. function runAdminActivityLotteryTests() { runBatchTests('admin-activity-lottery'); }
  1008. // ===================== 验证器 =====================
  1009. function validate_checkinStatus(result) { return result.data?.code === 200 && result.data?.data?.checkedToday !== undefined; }
  1010. function validate_checkinDo(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1011. function validate_checkinCalendar(result) { return result.data?.code === 200 && result.data?.data?.checkedDates !== undefined; }
  1012. function validate_checkinRecords(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1013. function validate_adminCheckinConfig(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1014. function validate_adminCheckinConfigUpdate(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1015. function validate_adminCheckinPeriods(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1016. function validate_adminCheckinPeriodCreate(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1017. function validate_adminCheckinPeriodUpdate(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1018. function validate_adminCheckinPeriodDelete(result) { return result.data?.code === 200; }
  1019. function validate_adminCheckinStats(result) { return result.data?.code === 200 && result.data?.data?.todayCheckinCount !== undefined; }
  1020. function validate_adminCheckinRecords(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1021. function validate_adminCheckinUserRecords(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1022. function validate_activityInviteStatus(result) { return result.data?.code === 200 && result.data?.data?.enabled !== undefined; }
  1023. function validate_activityCheckinStatus(result) { return result.data?.code === 200 && result.data?.data?.enabled !== undefined; }
  1024. function validate_activityLotteryStatus(result) { return result.data?.code === 200 && result.data?.data?.enabled !== undefined; }
  1025. function validate_activityLotteryPrizes(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1026. function validate_activityLotteryDraw(result) { return result.data?.code === 200 && result.data?.data?.prizeName !== undefined; }
  1027. function validate_adminActivityInviteConfig(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1028. function validate_adminActivityInviteRevoke(result) { return result.data?.code === 200 && result.data?.data !== undefined; }
  1029. function validate_adminActivityCheckinConfig(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1030. function validate_adminActivityLotteryConfig(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1031. function validate_adminActivityLotteryPrize(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1032. function validate_adminActivityLotteryPrizeCreate(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1033. function validate_adminActivityLotteryPrizeUpdate(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1034. function validate_adminActivityLotteryPrizeDelete(result) { return result.data?.code === 200; }
  1035. // ===================== 初始化 =====================
  1036. window.addEventListener('DOMContentLoaded', () => {
  1037. const params = new URLSearchParams(window.location.search);
  1038. if (params.get('baseUrl')) document.getElementById('baseUrl').value = params.get('baseUrl');
  1039. renderModule('checkin');
  1040. });
  1041. </script>
  1042. </body>
  1043. </html>