invite-test.html 113 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784
  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 #1976d2; 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; }
  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. .form-group textarea { min-height: 60px; resize: vertical; }
  56. .response-box { margin-top: 12px; background: #fafafa; border: 1px solid #e0e0e0; border-radius: 4px; padding: 12px; }
  57. .response-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
  58. .response-title { font-weight: 600; color: #333; font-size: 14px; }
  59. .status-badge { display: inline-block; padding: 4px 10px; border-radius: 3px; font-size: 12px; font-weight: 500; }
  60. .status-success { background: #c8e6c9; color: #2e7d32; }
  61. .status-error { background: #ffcdd2; color: #c62828; }
  62. .status-warn { 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. .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; }
  71. .summary-item { display: flex; align-items: center; gap: 6px; font-size: 14px; }
  72. .summary-count { font-weight: 700; font-size: 20px; }
  73. .summary-count.pass { color: #2e7d32; }
  74. .summary-count.fail { color: #c62828; }
  75. .summary-count.pending { color: #e65100; }
  76. .batch-buttons { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
  77. @media (max-width: 768px) {
  78. .form-grid { grid-template-columns: 1fr; }
  79. .toolbar { flex-direction: column; align-items: stretch; }
  80. .module-tab { font-size: 11px; padding: 8px 12px; }
  81. }
  82. .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); }
  83. .side-btn { flex: 1; padding: 16px 24px; border: none; cursor: pointer; font-size: 16px; font-weight: 600; transition: all 0.25s; text-align: center; }
  84. .side-btn.user-side { background: #e3f2fd; color: #1565c0; }
  85. .side-btn.user-side.active { background: #1565c0; color: white; }
  86. .side-btn.admin-side { background: #fff3e0; color: #e65100; }
  87. .side-btn.admin-side.active { background: #e65100; color: white; }
  88. .side-btn:hover { opacity: 0.9; transform: translateY(-1px); }
  89. .admin-login-area { background: #fff8e1; border: 1px solid #ffe0b2; border-radius: 6px; }
  90. .admin-login-area .login-title { color: #e65100 !important; }
  91. .admin-tabs .module-tab.active { color: #e65100; border-bottom-color: #e65100; }
  92. .admin-tabs .module-tab:hover { background: #fff3e0; color: #e65100; }
  93. /* 支付宝扫码支付弹窗 - DeepSeek 风格 */
  94. .alipay-modal-overlay {
  95. position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  96. background: rgba(0,0,0,0.45); z-index: 1000;
  97. display: flex; align-items: center; justify-content: center;
  98. backdrop-filter: blur(2px);
  99. }
  100. .alipay-modal {
  101. background: #fff; border-radius: 16px; width: 360px; max-width: 92vw;
  102. padding: 28px 24px 22px; box-shadow: 0 20px 60px rgba(0,0,0,0.18);
  103. text-align: center; position: relative; animation: modalIn 0.2s ease-out;
  104. }
  105. @keyframes modalIn {
  106. from { opacity: 0; transform: scale(0.95) translateY(10px); }
  107. to { opacity: 1; transform: scale(1) translateY(0); }
  108. }
  109. .alipay-modal-close {
  110. position: absolute; top: 14px; right: 16px; width: 28px; height: 28px;
  111. border: none; background: transparent; color: #999; font-size: 22px; line-height: 1;
  112. cursor: pointer; border-radius: 50%; transition: all 0.2s;
  113. }
  114. .alipay-modal-close:hover { background: #f5f5f5; color: #333; }
  115. .alipay-modal-logo {
  116. display: flex; align-items: center; justify-content: center; gap: 6px;
  117. margin-bottom: 16px; font-size: 18px; font-weight: 700; color: #1677ff;
  118. }
  119. .alipay-modal-logo svg { width: 26px; height: 26px; }
  120. .alipay-modal-title { font-size: 15px; color: #666; margin-bottom: 6px; }
  121. .alipay-modal-amount { font-size: 32px; font-weight: 700; color: #111; margin-bottom: 18px; }
  122. .alipay-modal-amount span { font-size: 18px; margin-right: 2px; }
  123. .alipay-modal-qr {
  124. width: 200px; height: 200px; margin: 0 auto 14px;
  125. border: 1px solid #f0f0f0; border-radius: 8px; padding: 8px; background: #fff;
  126. }
  127. .alipay-modal-qr img { width: 100%; height: 100%; object-fit: contain; }
  128. .alipay-modal-hint {
  129. font-size: 12px; color: #999; line-height: 1.6; margin-top: 8px;
  130. }
  131. .alipay-modal-status {
  132. margin-top: 10px; font-size: 13px; color: #1677ff; min-height: 20px;
  133. }
  134. </style>
  135. </head>
  136. <body>
  137. <div class="container">
  138. <h1>智价云药店 - 全量API验收测试工具</h1>
  139. <h2>覆盖用户侧 + 运营侧全量API | 独立登录管理 | 开发环境验证码固定 123456</h2>
  140. <!-- 用户侧/运营侧 切换器 -->
  141. <div class="side-switcher">
  142. <button class="side-btn user-side active" onclick="switchSide('user')">用户侧 (C端)</button>
  143. <button class="side-btn admin-side" onclick="switchSide('admin')">运营侧 (管理端)</button>
  144. </div>
  145. <!-- 全局工具栏 -->
  146. <div class="toolbar">
  147. <label>API 基础地址</label>
  148. <label for="baseUrl"></label><input type="text" id="baseUrl" value="http://8.135.32.236:8001" style="flex:1;">
  149. </div>
  150. <!-- 用户侧登录区 -->
  151. <div id="userLoginSection">
  152. <div class="toolbar">
  153. <label>用户Token</label>
  154. <label for="token"></label><input type="text" id="token" placeholder="登录后自动填入" style="flex:1;" readonly>
  155. <span id="roleBadge" class="role-tag" style="display:none;"></span>
  156. <button class="btn btn-outline btn-sm" onclick="copyToken()">复制Token</button>
  157. </div>
  158. <div class="toolbar" style="background:#f8f9fa;">
  159. <div class="login-section">
  160. <span class="login-title">短信登录</span>
  161. <label>手机号</label>
  162. <label for="phone"></label><input type="text" id="phone" value="18776156917" style="width:140px;">
  163. <label>验证码</label>
  164. <label for="smsCode"></label><input type="text" id="smsCode" placeholder="123456" value="123456" style="width:100px;">
  165. <button class="btn btn-primary btn-sm" onclick="sendSms()">发送验证码</button>
  166. <button class="btn btn-success btn-sm" onclick="smsLogin()">短信登录</button>
  167. </div>
  168. <div class="login-section">
  169. <span class="login-title">密码登录</span>
  170. <label>手机号/用户名</label>
  171. <label for="pwdAccount"></label><input type="text" id="pwdAccount" value="18776156917" placeholder="手机号或用户名" style="width:140px;">
  172. <label>密码</label>
  173. <label for="pwdPassword"></label><input type="password" id="pwdPassword" placeholder="输入密码" style="width:120px;">
  174. <button class="btn btn-success btn-sm" onclick="passwordLogin()">密码登录</button>
  175. </div>
  176. </div>
  177. </div>
  178. <!-- 运营侧登录区 -->
  179. <div id="adminLoginSection" style="display:none;">
  180. <div class="toolbar">
  181. <label>管理员Token</label>
  182. <label for="adminToken"></label><input type="text" id="adminToken" placeholder="管理员登录后自动填入" style="flex:1;" readonly>
  183. <span id="adminRoleBadge" class="role-tag" style="display:none;"></span>
  184. <button class="btn btn-outline btn-sm" onclick="copyToken()">复制Token</button>
  185. </div>
  186. <div class="toolbar admin-login-area">
  187. <div class="login-section">
  188. <span class="login-title">管理员短信登录</span>
  189. <label>手机号</label>
  190. <label for="adminPhone"></label><input type="text" id="adminPhone" value="" placeholder="管理员手机号" style="width:150px;">
  191. <label>验证码</label>
  192. <label for="adminSmsCode"></label><input type="text" id="adminSmsCode" placeholder="123456" value="123456" style="width:100px;">
  193. <button class="btn btn-primary btn-sm" onclick="sendSms()">发送验证码</button>
  194. <button class="btn btn-warning btn-sm" onclick="smsLogin()">管理员短信登录</button>
  195. </div>
  196. <div class="login-section">
  197. <span class="login-title">管理员密码登录</span>
  198. <label>用户名</label>
  199. <label for="adminPwdUsername"></label><input type="text" id="adminPwdUsername" value="admin" placeholder="管理员用户名" style="width:150px;">
  200. <label>密码</label>
  201. <label for="adminPwdPassword"></label><input type="password" id="adminPwdPassword" value="admin@123" placeholder="管理员密码" style="width:140px;">
  202. <button class="btn btn-warning btn-sm" onclick="passwordLogin()">管理员密码登录</button>
  203. </div>
  204. </div>
  205. </div>
  206. <!-- 测试统计栏 -->
  207. <div class="summary-bar">
  208. <span style="font-weight:600;">本页测试统计:</span>
  209. <div class="summary-item"><span style="font-weight:700;font-size:20px;color:#1565c0;" id="totalCount">0</span><span>总计</span></div>
  210. <div class="summary-item"><span class="summary-count pass" id="passCount">0</span><span>通过</span></div>
  211. <div class="summary-item"><span class="summary-count fail" id="failCount">0</span><span>失败</span></div>
  212. <div class="summary-item"><span class="summary-count pending" id="pendingCount">0</span><span>待测</span></div>
  213. <button class="btn btn-warning btn-sm" onclick="runAllTests()" style="margin-left:auto;">一键全部测试</button>
  214. <button class="btn btn-outline btn-sm" onclick="clearAllResults()">清除结果</button>
  215. </div>
  216. <!-- 用户侧模块标签栏 -->
  217. <div class="module-tabs" id="moduleTabs">
  218. <div class="module-tab active" data-module="auth">1.认证模块</div>
  219. <div class="module-tab" data-module="crawler">2.爬虫配额</div>
  220. <div class="module-tab" data-module="invite">3.邀请系统</div>
  221. <div class="module-tab" data-module="coupon">4.优惠券</div>
  222. <div class="module-tab" data-module="membership">5.会员体系</div>
  223. <div class="module-tab" data-module="payment">6.支付模块</div>
  224. <div class="module-tab" data-module="upload">7.文件上传</div>
  225. <div class="module-tab" data-module="search">8.搜索查询</div>
  226. <div class="module-tab" data-module="platform-account">9.平台账号</div>
  227. <div class="module-tab" data-module="business-license">10.营业执照</div>
  228. <div class="module-tab" data-module="platform-config">11.平台配置</div>
  229. <div class="module-tab" data-module="search-record">12.搜索记录</div>
  230. <div class="module-tab" data-module="platform-session">13.平台会话</div>
  231. </div>
  232. <!-- 运营侧模块标签栏 -->
  233. <div class="module-tabs admin-tabs" id="adminModuleTabs" style="display:none;">
  234. <div class="module-tab active" data-module="admin-membership">1.会员管理</div>
  235. <div class="module-tab" data-module="admin-invite">2.邀请管理</div>
  236. <div class="module-tab" data-module="admin-crawler">3.爬虫管理</div>
  237. <div class="module-tab" data-module="admin-coupon">4.优惠券发放</div>
  238. <div class="module-tab" data-module="admin-license">5.执照审核</div>
  239. <div class="module-tab" data-module="admin-payment">6.支付方案</div>
  240. <div class="module-tab" data-module="admin-trial-quota">7.体验配额</div>
  241. <div class="module-tab" data-module="admin-user">8.用户管理</div>
  242. <div class="module-tab" data-module="admin-audit">9.审计日志</div>
  243. <div class="module-tab" data-module="upload">10.文件上传</div>
  244. </div>
  245. <div id="panels"></div>
  246. </div>
  247. <script>
  248. // ===================== 全局状态 =====================
  249. let activeSide = 'user';
  250. const BASE_URL = () => document.getElementById('baseUrl').value;
  251. const TOKEN = () => activeSide === 'admin'
  252. ? (document.getElementById('adminToken')?.value || '')
  253. : (document.getElementById('token')?.value || '');
  254. const PHONE = () => activeSide === 'admin'
  255. ? (document.getElementById('adminPhone')?.value || '')
  256. : (document.getElementById('phone')?.value || '');
  257. const SMS_CODE = () => activeSide === 'admin'
  258. ? (document.getElementById('adminSmsCode')?.value || '123456')
  259. : (document.getElementById('smsCode')?.value || '123456');
  260. let stats = { pass: 0, fail: 0, pending: 0, total: 0 };
  261. // ===================== 大整数安全JSON解析 =====================
  262. /**
  263. * 安全解析JSON:将16位以上的大整数(如雪花ID)包裹为字符串后再解析,
  264. * 避免JavaScript Number精度丢失(如2069620495151230978 → 2069620495151231000)
  265. * 示例:":2069620495151230978," → ":\"2069620495151230978\","
  266. */
  267. function safeJsonParse(text) {
  268. const safeText = text.replace(/([:,\[]\s*)(\d{16,})(\s*[,\]\}])/g, '$1"$2"$3');
  269. return JSON.parse(safeText);
  270. }
  271. // ===================== HTTP 请求 =====================
  272. async function api(method, path, body, requireAuth = true) {
  273. const headers = { 'Content-Type': 'application/json' };
  274. if (requireAuth && TOKEN()) { headers['Authorization'] = 'Bearer ' + TOKEN(); }
  275. const opts = { method, headers };
  276. if (body && method !== 'GET') { opts.body = JSON.stringify(body); }
  277. const startTime = Date.now();
  278. try {
  279. const res = await fetch(BASE_URL() + path, opts);
  280. const text = await res.text();
  281. let data;
  282. try { data = safeJsonParse(text); } catch (e) { data = { _raw: text }; }
  283. return { ok: res.ok, status: res.status, data, duration: Date.now() - startTime };
  284. } catch (e) {
  285. return { ok: false, status: 0, data: { error: e.message }, duration: Date.now() - startTime };
  286. }
  287. }
  288. // ===================== 测试执行 =====================
  289. async function runTest(sectionId, method, path, body, requireAuth, validatorName) {
  290. const box = document.getElementById(sectionId + '-box');
  291. const statusEl = document.getElementById(sectionId + '-status');
  292. const rawEl = document.getElementById(sectionId + '-raw');
  293. const durEl = document.getElementById(sectionId + '-duration');
  294. updateBadge(statusEl, 'pending', '运行中...');
  295. if (box) box.style.display = 'block';
  296. let resolvedPath = path;
  297. document.querySelectorAll(`[id^="${sectionId}-"]`).forEach(input => {
  298. if (input.tagName === 'INPUT') {
  299. const fieldName = input.id.replace(sectionId + '-', '');
  300. // 替换路径变量 {fieldName}
  301. resolvedPath = resolvedPath.replace('{' + fieldName + '}', input.value || fieldName);
  302. // 替换查询参数 ?fieldName=xxx 或 &fieldName=xxx(输入框有值时才替换)
  303. if (input.value) {
  304. const queryVal = encodeURIComponent(input.value);
  305. resolvedPath = resolvedPath.replace(
  306. new RegExp('([?&])' + fieldName + '=[^&]*', 'g'),
  307. '$1' + fieldName + '=' + queryVal
  308. );
  309. }
  310. }
  311. });
  312. const result = await api(method, resolvedPath, body, requireAuth);
  313. if (durEl) durEl.textContent = result.duration + 'ms';
  314. if (rawEl) rawEl.textContent = JSON.stringify(result.data, null, 2);
  315. // 检测响应中的支付结果并渲染
  316. const qrBase64 = result.data?.data?.qrCodeBase64;
  317. const payUrl = result.data?.data?.payUrl;
  318. const channel = result.data?.data?.channel;
  319. const orderNo = result.data?.data?.orderNo;
  320. const amount = result.data?.data?.amount;
  321. // 自动回填订单号到相关输入框
  322. if (orderNo) {
  323. ['payment-query-order-orderNo', 'payment-mock-pay-orderNo',
  324. 'payment-callback-wechat-out_trade_no', 'payment-callback-alipay-out_trade_no']
  325. .forEach(fid => { const el = document.getElementById(fid); if (el) el.value = orderNo; });
  326. }
  327. if (channel === 'ALIPAY' && qrBase64) {
  328. // 支付宝:DeepSeek 风格自定义弹窗,展示二维码
  329. showAlipayModal(qrBase64, amount, orderNo);
  330. } else if (qrBase64) {
  331. // 微信:显示扫码二维码
  332. let qrBox = document.getElementById(sectionId + '-qrcode');
  333. if (!qrBox) {
  334. qrBox = document.createElement('div');
  335. qrBox.id = sectionId + '-qrcode';
  336. qrBox.style.cssText = 'text-align:center;padding:12px;background:#fff;border-radius:4px;margin-top:8px;';
  337. rawEl.parentNode.insertBefore(qrBox, rawEl.nextSibling);
  338. }
  339. qrBox.innerHTML = '<img src="' + qrBase64 + '" style="max-width:220px;border:1px solid #eee;border-radius:4px;"><div style="margin-top:6px;font-size:12px;color:#666;">微信扫码付款后回调自动激活,也可手动确认支付测试回调</div>';
  340. } else {
  341. const oldBox = document.getElementById(sectionId + '-qrcode');
  342. if (oldBox) oldBox.remove();
  343. }
  344. let passed = false;
  345. if (validatorName && typeof window[validatorName] === 'function') {
  346. try { passed = window[validatorName](result); } catch (e) { passed = false; }
  347. } else {
  348. passed = result.ok && result.status === 200;
  349. }
  350. updateStats(sectionId, passed);
  351. updateBadge(statusEl, passed ? 'success' : 'error', passed ? '通过 ' + result.status : '失败 ' + result.status);
  352. if (box) box.style.display = 'block';
  353. }
  354. function updateBadge(el, cls, text) {
  355. if (!el) return;
  356. el.textContent = text;
  357. el.className = 'status-badge status-' + cls;
  358. }
  359. function updateStats(id, passed) {
  360. const old = stats[id];
  361. if (old === 'pass') stats.pass--;
  362. if (old === 'fail') stats.fail--;
  363. stats[id] = passed ? 'pass' : 'fail';
  364. if (passed) stats.pass++; else stats.fail++;
  365. stats.pending = Math.max(0, stats.total - stats.pass - stats.fail);
  366. document.getElementById('passCount').textContent = stats.pass;
  367. document.getElementById('failCount').textContent = stats.fail;
  368. document.getElementById('pendingCount').textContent = stats.pending;
  369. }
  370. // ===================== 登录相关 =====================
  371. async function sendSms() {
  372. const res = await api('POST', '/api/auth/sms/send', { phone: PHONE(), scene: 'login' }, false);
  373. if (res.ok) {
  374. if (activeSide === 'admin') {
  375. const el = document.getElementById('adminSmsCode');
  376. if (el) el.value = '123456';
  377. } else {
  378. document.getElementById('smsCode').value = '123456';
  379. }
  380. const label = activeSide === 'admin' ? '管理员验证码' : '验证码';
  381. alert(label + '已写入(开发测试模式,固定验证码: 123456)');
  382. } else {
  383. alert('发送失败: ' + JSON.stringify(res.data));
  384. }
  385. }
  386. async function smsLogin() {
  387. const phone = PHONE();
  388. const code = SMS_CODE();
  389. if (!phone) { alert(activeSide === 'admin' ? '请填写管理员手机号' : '请填写手机号'); return; }
  390. const payload = { phone, code, scene: 'login' };
  391. const res = await api('POST', '/api/auth/sms/login', payload, false);
  392. if (res.data?.code === 200 && res.data?.data?.accessToken) {
  393. if (activeSide === 'admin') {
  394. document.getElementById('adminToken').value = res.data.data.accessToken;
  395. updateAdminRoleBadge(res.data.data.role);
  396. alert('管理员短信登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'USER'));
  397. } else {
  398. document.getElementById('token').value = res.data.data.accessToken;
  399. updateRoleBadge(res.data.data.role);
  400. alert('短信登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'USER'));
  401. }
  402. } else {
  403. alert('登录失败: ' + (res.data?.message || '未知错误'));
  404. }
  405. }
  406. async function passwordLogin() {
  407. const password = activeSide === 'admin'
  408. ? document.getElementById('adminPwdPassword').value
  409. : document.getElementById('pwdPassword').value;
  410. let payload;
  411. if (activeSide === 'admin') {
  412. const username = document.getElementById('adminPwdUsername').value;
  413. if (!username || !password) { alert('请填写用户名和密码'); return; }
  414. payload = { username, password };
  415. } else {
  416. const account = document.getElementById('pwdAccount').value.trim();
  417. if (!account || !password) { alert('请填写手机号/用户名和密码'); return; }
  418. // 自动识别:纯数字视为手机号,否则视为用户名
  419. if (/^\d{11}$/.test(account)) {
  420. payload = { phone: account, password };
  421. } else {
  422. payload = { username: account, password };
  423. }
  424. }
  425. const res = await api('POST', '/api/auth/password/login', payload, false);
  426. if (res.data?.code === 200 && res.data?.data?.accessToken) {
  427. if (activeSide === 'admin') {
  428. document.getElementById('adminToken').value = res.data.data.accessToken;
  429. updateAdminRoleBadge(res.data.data.role);
  430. alert('管理员密码登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'USER'));
  431. } else {
  432. document.getElementById('token').value = res.data.data.accessToken;
  433. updateRoleBadge(res.data.data.role);
  434. alert('密码登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'USER'));
  435. }
  436. } else {
  437. alert('登录失败: ' + (res.data?.message || '未知错误'));
  438. }
  439. }
  440. function updateRoleBadge(role) {
  441. const badge = document.getElementById('roleBadge');
  442. if (badge) {
  443. if (role) {
  444. badge.textContent = role;
  445. badge.style.display = 'inline-block';
  446. } else {
  447. badge.style.display = 'none';
  448. }
  449. }
  450. }
  451. function updateAdminRoleBadge(role) {
  452. const badge = document.getElementById('adminRoleBadge');
  453. if (badge) {
  454. if (role) {
  455. badge.textContent = role;
  456. badge.style.display = 'inline-block';
  457. } else {
  458. badge.style.display = 'none';
  459. }
  460. }
  461. }
  462. function copyToken() {
  463. const tok = TOKEN();
  464. if (!tok) { alert('请先登录'); return; }
  465. navigator.clipboard.writeText(tok).then(() => alert('Token已复制!'));
  466. }
  467. async function sendCodeToField(inputId, scene) {
  468. const phoneInput = document.getElementById(inputId);
  469. if (!phoneInput || !phoneInput.value) { alert('请先填写手机号'); return; }
  470. const res = await api('POST', '/api/auth/sms/send', { phone: phoneInput.value, scene: scene || 'login' }, false);
  471. if (res.ok) {
  472. alert('验证码已发送(开发测试模式,固定验证码: 123456)');
  473. } else {
  474. alert('发送失败: ' + JSON.stringify(res.data));
  475. }
  476. }
  477. async function uploadFile() {
  478. const fileInput = document.getElementById('upload-file');
  479. const statusEl = document.getElementById('upload-status');
  480. const rawEl = document.getElementById('upload-raw');
  481. const durEl = document.getElementById('upload-duration');
  482. const box = document.getElementById('upload-result-box');
  483. if (!fileInput || !fileInput.files || !fileInput.files[0]) {
  484. alert('请先选择文件'); return;
  485. }
  486. const formData = new FormData();
  487. formData.append('file', fileInput.files[0]);
  488. updateBadge(statusEl, 'pending', '上传中...');
  489. box.style.display = 'block';
  490. const startTime = Date.now();
  491. try {
  492. const headers = {};
  493. if (TOKEN()) { headers['Authorization'] = 'Bearer ' + TOKEN(); }
  494. const res = await fetch(BASE_URL() + '/api/upload', { method: 'POST', headers, body: formData });
  495. const text = await res.text();
  496. const duration = Date.now() - startTime;
  497. let data;
  498. try { data = safeJsonParse(text); } catch (e) { data = { _raw: text }; }
  499. if (durEl) durEl.textContent = duration + 'ms';
  500. if (rawEl) rawEl.textContent = JSON.stringify(data, null, 2);
  501. const passed = res.ok && data?.code === 200;
  502. updateBadge(statusEl, passed ? 'success' : 'error', passed ? '通过 ' + res.status : '失败 ' + res.status);
  503. if (!stats.hasOwnProperty('upload-file')) {
  504. stats.total++;
  505. document.getElementById('totalCount').textContent = stats.total;
  506. }
  507. updateStats('upload-file', passed);
  508. } catch (e) {
  509. if (rawEl) rawEl.textContent = 'Error: ' + e.message;
  510. updateBadge(statusEl, 'error', '失败');
  511. }
  512. }
  513. // ===================== 侧切换 =====================
  514. function switchSide(side) {
  515. activeSide = side;
  516. document.querySelectorAll('.side-btn').forEach(b => b.classList.remove('active'));
  517. document.querySelector('.side-btn.' + side + '-side').classList.add('active');
  518. const isAdmin = side === 'admin';
  519. document.getElementById('userLoginSection').style.display = isAdmin ? 'none' : '';
  520. document.getElementById('adminLoginSection').style.display = isAdmin ? '' : 'none';
  521. document.getElementById('moduleTabs').style.display = isAdmin ? 'none' : '';
  522. document.getElementById('adminModuleTabs').style.display = isAdmin ? '' : 'none';
  523. if (isAdmin) {
  524. const activeTab = document.querySelector('#adminModuleTabs .module-tab.active');
  525. renderModule(activeTab ? activeTab.dataset.module : 'admin-membership');
  526. } else {
  527. const activeTab = document.querySelector('#moduleTabs .module-tab.active');
  528. renderModule(activeTab ? activeTab.dataset.module : 'auth');
  529. }
  530. }
  531. // ===================== 模块切换 =====================
  532. document.getElementById('moduleTabs').addEventListener('click', (e) => {
  533. if (!e.target.classList.contains('module-tab')) return;
  534. document.querySelectorAll('#moduleTabs .module-tab').forEach(t => t.classList.remove('active'));
  535. e.target.classList.add('active');
  536. renderModule(e.target.dataset.module);
  537. });
  538. document.getElementById('adminModuleTabs').addEventListener('click', (e) => {
  539. if (!e.target.classList.contains('module-tab')) return;
  540. document.querySelectorAll('#adminModuleTabs .module-tab').forEach(t => t.classList.remove('active'));
  541. e.target.classList.add('active');
  542. renderModule(e.target.dataset.module);
  543. });
  544. // ===================== 渲染模块面板 =====================
  545. function renderModule(module) {
  546. const panels = document.getElementById('panels');
  547. let html = '';
  548. switch (module) {
  549. // 用户侧模块
  550. case 'auth': html = renderAuthModule(); break;
  551. case 'crawler': html = renderCrawlerModule(); break;
  552. case 'invite': html = renderInviteModule(); break;
  553. case 'coupon': html = renderCouponModule(); break;
  554. case 'membership': html = renderMembershipModule(); break;
  555. case 'payment': html = renderPaymentModule(); break;
  556. case 'upload': html = renderUploadModule(); break;
  557. case 'search': html = renderSearchModule(); break;
  558. case 'platform-account': html = renderPlatformAccountModule(); break;
  559. case 'business-license': html = renderBusinessLicenseModule(); break;
  560. case 'platform-config': html = renderPlatformConfigModule(); break;
  561. case 'search-record': html = renderSearchRecordModule(); break;
  562. case 'platform-session': html = renderPlatformSessionModule(); break;
  563. // 运营侧模块
  564. case 'admin-membership': html = renderAdminMembershipModule(); break;
  565. case 'admin-invite': html = renderAdminInviteModule(); break;
  566. case 'admin-crawler': html = renderAdminCrawlerModule(); break;
  567. case 'admin-coupon': html = renderAdminCouponModule(); break;
  568. case 'admin-license': html = renderAdminLicenseModule(); break;
  569. case 'admin-payment': html = renderAdminPaymentModule(); break;
  570. case 'admin-trial-quota': html = renderAdminTrialQuotaModule(); break;
  571. case 'admin-user': html = renderAdminUserModule(); break;
  572. case 'admin-audit': html = renderAdminAuditModule(); break;
  573. }
  574. panels.innerHTML = html;
  575. }
  576. function toValidatorName(id) {
  577. const parts = id.split('-');
  578. let camel = parts[0];
  579. for (let i = 1; i < parts.length; i++) {
  580. camel += parts[i].charAt(0).toUpperCase() + parts[i].slice(1);
  581. }
  582. return 'validate_' + camel;
  583. }
  584. // ==================== 字段说明数据 ====================
  585. const FIELD_DESC = {
  586. 'resultWrapper': [
  587. ['code', 'Integer', '业务状态码(200=成功)'],
  588. ['message', 'String', '提示信息(如:操作成功)'],
  589. ['data', 'Object/Array/null', '返回数据(详见下方字段)'],
  590. ['timestamp', 'Long', '响应时间戳(毫秒级)']
  591. ],
  592. 'loginResponse': [
  593. ['userId', 'Long', '用户ID'],
  594. ['accessToken', 'String', 'JWT访问令牌'],
  595. ['refreshToken', 'String', '刷新令牌'],
  596. ['expiresIn', 'Long', '令牌过期时间(秒)'],
  597. ['userInfo', 'Object', '用户信息(嵌套对象,见下方子字段)'],
  598. ['userInfo.id', 'Long', '用户ID(主键)'],
  599. ['userInfo.userId', 'Long', '用户ID(兼容字段=id)'],
  600. ['userInfo.phone', 'String', '手机号(脱敏 187****6917)'],
  601. ['userInfo.nickname', 'String', '昵称(如:用户6917)'],
  602. ['userInfo.avatar', 'String', '头像URL(可能为null)'],
  603. ['userInfo.wechatBound', 'Boolean', '是否已绑定微信'],
  604. ['userInfo.levelInfo', 'Object', '等级信息(嵌套对象,见下方子字段)'],
  605. ['userInfo.levelInfo.id', 'Long', '等级ID'],
  606. ['userInfo.levelInfo.levelName', 'String', '等级名称(如:普通会员)'],
  607. ['userInfo.levelInfo.levelCode', 'String', '等级编码(如:PLUS)'],
  608. ['userInfo.levelInfo.levelWeight', 'Integer', '等级权重(越高越好)'],
  609. ['userInfo.levelInfo.crawlerQuota', 'Integer', '每日爬虫配额'],
  610. ['userInfo.levelInfo.monthlyQuota', 'Integer', '每月爬虫配额'],
  611. ['userInfo.levelInfo.maxConcurrent', 'Integer', '最大并发数'],
  612. ['userInfo.levelInfo.description', 'String', '等级描述'],
  613. ['userInfo.levelInfo.version', 'String', '配置版本号(可能为null)'],
  614. ['userInfo.levelInfo.membershipExpireAt', 'DateTime', '会员到期时间(null=永久或无会员)'],
  615. ['userInfo.levelInfo.membershipDaysRemaining', 'Integer', '会员剩余天数(-1=永久,0=已过期)'],
  616. ['userInfo.levelInfo.pricingPlans', 'Array', '当前等级套餐定价方案列表'],
  617. ['userInfo.levelInfo.pricingPlans[].planCode', 'String', '方案编码(MONTHLY_PLUS/YEARLY_PLUS等)'],
  618. ['userInfo.levelInfo.pricingPlans[].planName', 'String', '方案名称(普通会员·月卡)'],
  619. ['userInfo.levelInfo.pricingPlans[].price', 'BigDecimal', '价格(元,如9.90)'],
  620. ['userInfo.levelInfo.pricingPlans[].originalPrice', 'BigDecimal', '原价(元,如19.90)'],
  621. ['userInfo.levelInfo.pricingPlans[].durationDays', 'Integer', '会员有效天数(30/365)'],
  622. ['userInfo.levelInfo.pricingPlans[].description', 'String', '方案描述'],
  623. ['userInfo.createTime', 'DateTime', '注册时间'],
  624. ['userInfo.loginSource', 'String', '登录来源(SMS/PASSWORD/WECHAT,可能为null)'],
  625. ['userInfo.levelCode', 'String', '等级编码(冗余字段)'],
  626. ['userInfo.version', 'String', '等级版本(冗余字段,可能为null)'],
  627. ['needBindPhone', 'Boolean', '是否需要绑定手机号'],
  628. ['tempToken', 'String', '临时令牌(用于绑定手机号,通常为null)'],
  629. ['newUser', 'Boolean', '是否为新注册用户']
  630. ],
  631. 'userInfo': [
  632. ['id', 'Long', '用户ID(主键)'],
  633. ['userId', 'Long', '用户ID(兼容字段=id)'],
  634. ['phone', 'String', '手机号(脱敏 187****6917)'],
  635. ['nickname', 'String', '昵称(如:用户6917)'],
  636. ['avatar', 'String', '头像URL(可能为null)'],
  637. ['wechatBound', 'Boolean', '是否已绑定微信'],
  638. ['levelInfo', 'Object', '等级信息(嵌套对象,见下方子字段)'],
  639. ['levelInfo.id', 'Long', '等级ID'],
  640. ['levelInfo.levelName', 'String', '等级名称(如:普通会员)'],
  641. ['levelInfo.levelCode', 'String', '等级编码(如:PLUS)'],
  642. ['levelInfo.levelWeight', 'Integer', '等级权重(越高越好)'],
  643. ['levelInfo.crawlerQuota', 'Integer', '每日爬虫配额'],
  644. ['levelInfo.monthlyQuota', 'Integer', '每月爬虫配额'],
  645. ['levelInfo.maxConcurrent', 'Integer', '最大并发数'],
  646. ['levelInfo.description', 'String', '等级描述'],
  647. ['levelInfo.version', 'String', '配置版本号(可能为null)'],
  648. ['levelInfo.membershipExpireAt', 'DateTime', '会员到期时间(null=永久或无会员)'],
  649. ['levelInfo.membershipDaysRemaining', 'Integer', '会员剩余天数(-1=永久,0=已过期)'],
  650. ['levelInfo.pricingPlans', 'Array', '当前等级套餐定价方案列表'],
  651. ['levelInfo.pricingPlans[].planCode', 'String', '方案编码(MONTHLY_PLUS/YEARLY_PLUS等)'],
  652. ['levelInfo.pricingPlans[].planName', 'String', '方案名称(普通会员·月卡)'],
  653. ['levelInfo.pricingPlans[].price', 'BigDecimal', '价格(元,如9.90)'],
  654. ['levelInfo.pricingPlans[].originalPrice', 'BigDecimal', '原价(元,如19.90)'],
  655. ['levelInfo.pricingPlans[].durationDays', 'Integer', '会员有效天数(30/365)'],
  656. ['levelInfo.pricingPlans[].description', 'String', '方案描述'],
  657. ['membershipLevel', 'String', '会员等级(PLUS/PRO/ULTRA,可能为null)'],
  658. ['membershipLevelName', 'String', '会员等级中文名(普通会员/高级会员/超级会员)'],
  659. ['createTime', 'DateTime', '注册时间'],
  660. ['loginSource', 'String', '登录来源(SMS/PASSWORD/WECHAT,可能为null)'],
  661. ['levelCode', 'String', '等级编码(冗余字段,来自levelInfo,如:PLUS)']
  662. ],
  663. 'crawlerStatus': [
  664. ['levelName', 'String', '等级名称(如:普通用户)'],
  665. ['levelCode', 'String', '等级编码(如:NORMAL)'],
  666. ['todayUsed', 'Integer', '今日已用次数'],
  667. ['todayQuota', 'Integer', '今日总配额(等级+额外)'],
  668. ['todayRemaining', 'Integer', '今日剩余次数'],
  669. ['monthUsed', 'Integer', '本月已用次数'],
  670. ['monthQuota', 'Integer', '本月总配额(等级+额外)'],
  671. ['monthRemaining', 'Integer', '本月剩余次数'],
  672. ['baseDailyQuota', 'Integer', '等级基础每日配额'],
  673. ['baseMonthlyQuota', 'Integer', '等级基础每月配额'],
  674. ['extraQuota', 'Integer', '额外配额总额(三种来源之和)'],
  675. ['extraQuotaBreakdown', 'Array', '额外配额来源明细(见下方)'],
  676. ['extraQuotaBreakdown[].sourceType', 'String', '来源:INVITE/PURCHASE/ADMIN'],
  677. ['extraQuotaBreakdown[].sourceName', 'String', '来源名称(邀请奖励/购买会员/运营发放)'],
  678. ['extraQuotaBreakdown[].available', 'Integer', '该来源可用次数'],
  679. ['extraQuotaBreakdown[].total', 'Integer', '该来源累计获得'],
  680. ['extraQuotaBreakdown[].used', 'Integer', '该来源已使用'],
  681. ['maxConcurrent', 'Integer', '最大并发数'],
  682. ['hasCoupon', 'Boolean', '是否有可用优惠券'],
  683. ['availableCouponCount', 'Integer', '可用优惠券数量']
  684. ],
  685. 'crawlerLog': [
  686. ['id', 'Long', '记录ID'],
  687. ['userId', 'Long', '用户ID'],
  688. ['usageDate', 'Date', '使用日期'],
  689. ['usageCount', 'Integer', '本次使用次数'],
  690. ['platform', 'String', '爬取平台代码'],
  691. ['source', 'String', '消耗来源(QUOTA/COUPON)'],
  692. ['remark', 'String', '备注'],
  693. ['createTime', 'DateTime', '创建时间']
  694. ],
  695. 'inviteCode': [
  696. ['code', 'String', '邀请码(8位)'],
  697. ['inviteLink', 'String', '完整邀请链接'],
  698. ['inviterNickname', 'String', '邀请人昵称'],
  699. ['usedCount', 'Integer', '已使用次数(注册)'],
  700. ['clickCount', 'Integer', '链接被打开次数'],
  701. ['maxUses', 'Integer', '最大使用次数'],
  702. ['expireTime', 'DateTime', '过期时间'],
  703. ['status', 'Integer', '状态'],
  704. ['createTime', 'DateTime', '创建时间'],
  705. ['shareTitle', 'String', '分享标题'],
  706. ['shareDescription', 'String', '分享描述'],
  707. ['copyText', 'String', '一键复制文本'],
  708. ['rewardDescription', 'String', '奖励说明']
  709. ],
  710. 'inviteStats': [
  711. ['myInviteCode', 'String', '我的邀请码'],
  712. ['inviteLink', 'String', '邀请链接'],
  713. ['clickedCount', 'Integer', '链接被打开总次数'],
  714. ['totalInvited', 'Integer', '已邀请人数'],
  715. ['registeredCount', 'Integer', '已注册人数'],
  716. ['totalReward', 'Integer', '累计获得奖励会员月数'],
  717. ['summaryText', 'String', '摘要文本'],
  718. ['todayInvited', 'Integer', '今日已邀请人数'],
  719. ['todayRemaining', 'Integer', '今日剩余可邀次数'],
  720. ['rewardPerInvite', 'Integer', '每次邀请奖励会员月数']
  721. ],
  722. 'inviteReward': [
  723. ['inviterId', 'Long', '邀请人ID'],
  724. ['inviterNickname', 'String', '邀请人昵称'],
  725. ['inviteeId', 'Long', '被邀请人ID'],
  726. ['inviteeNickname', 'String', '被邀请人昵称'],
  727. ['registered', 'Integer', '是否已注册(0仅点击/1已注册)'],
  728. ['registeredText', 'String', '注册状态文本'],
  729. ['rewardCount', 'Integer', '奖励会员月数'],
  730. ['inviteTime', 'String', '邀请时间']
  731. ],
  732. 'invitePage': [
  733. ['inviterNickname', 'String', '邀请人昵称'],
  734. ['inviterAvatar', 'String', '邀请人头像'],
  735. ['inviteCode', 'String', '邀请码'],
  736. ['channel', 'String', '渠道(windows/app/wechat/miniapp等,默认windows)'],
  737. ['openType', 'String', '打开方式(download/redirect等)'],
  738. ['appName', 'String', '应用名称'],
  739. ['landingTitle', 'String', '落地页标题'],
  740. ['landingDesc', 'String', '落地页描述'],
  741. ['appDownloadUrl', 'String', 'Windows下载地址(channel=windows/app)'],
  742. ['miniappPath', 'String', '小程序路径'],
  743. ['miniappAppId', 'String', '小程序AppID'],
  744. ['wechatRedirectUrl', 'String', '微信跳转URL'],
  745. ['dingtalkAppId', 'String', '钉钉AppID'],
  746. ['feishuAppId', 'String', '飞书AppID'],
  747. ['rewardPerRegister', 'Integer', '每次注册奖励会员月数'],
  748. ['autoFillSupported', 'Boolean', '是否支持自动填入'],
  749. ['urlScheme', 'String', 'URL Scheme'],
  750. ['deepLink', 'String', '深度链接'],
  751. ['instructionText', 'String', '提示文案']
  752. ],
  753. 'couponIssue': [
  754. ['data', 'Integer', '成功发放的优惠券数量(如:2表示2张)']
  755. ],
  756. 'coupon': [
  757. ['id', 'Long', '优惠券ID'],
  758. ['couponName', 'String', '优惠券名称'],
  759. ['type', 'String', '类型(CRAWLER等)'],
  760. ['value', 'Integer', '券面值(次数)'],
  761. ['status', 'Integer', '状态(0未用/1已用/2过期)'],
  762. ['expireTime', 'DateTime', '过期时间'],
  763. ['usedTime', 'DateTime', '使用时间'],
  764. ['source', 'String', '来源'],
  765. ['createTime', 'DateTime', '领取时间']
  766. ],
  767. 'membership': [
  768. ['id', 'Long', '会员记录ID'],
  769. ['userId', 'Long', '用户ID'],
  770. ['level', 'String', '会员等级(PLUS/PRO/ULTRA)'],
  771. ['levelName', 'String', '等级中文名'],
  772. ['source', 'String', '来源(ADMIN_GRANT/RECHARGE/SYSTEM)'],
  773. ['effectiveFrom', 'DateTime', '生效开始时间'],
  774. ['effectiveTo', 'DateTime', '生效结束时间(NULL表示永久)'],
  775. ['status', 'Integer', '状态(1有效/0过期停用)'],
  776. ['remark', 'String', '备注'],
  777. ['createTime', 'DateTime', '创建时间']
  778. ],
  779. 'membershipBenefit': [
  780. ['benefitType', 'String', '权益类型(DAILY_QUOTA/MONTHLY_QUOTA/MAX_CONCURRENT)'],
  781. ['benefitTypeName', 'String', '权益类型中文名'],
  782. ['benefitValue', 'Integer', '权益数值'],
  783. ['description', 'String', '权益说明']
  784. ],
  785. 'levelResponse': [
  786. ['id', 'Long', '等级ID'],
  787. ['levelName', 'String', '等级名称'],
  788. ['levelCode', 'String', '等级编码'],
  789. ['levelWeight', 'Integer', '等级权重(越高越好)'],
  790. ['crawlerQuota', 'Integer', '每日爬虫配额'],
  791. ['monthlyQuota', 'Integer', '每月爬虫配额'],
  792. ['maxConcurrent', 'Integer', '最大并发数'],
  793. ['description', 'String', '等级描述'],
  794. ['version', 'String', '配置版本号'],
  795. ['membershipExpireAt', 'DateTime', '会员到期时间(null=永久或无会员)'],
  796. ['membershipDaysRemaining', 'Integer', '会员剩余天数(-1=永久,0=已过期)'],
  797. ['pricingPlans', 'Array', '当前等级套餐定价方案列表'],
  798. ['pricingPlans[].planCode', 'String', '方案编码(MONTHLY_PLUS/YEARLY_PLUS等)'],
  799. ['pricingPlans[].planName', 'String', '方案名称(普通会员·月卡)'],
  800. ['pricingPlans[].price', 'BigDecimal', '价格(元,如9.90)'],
  801. ['pricingPlans[].originalPrice', 'BigDecimal', '原价(元,如19.90)'],
  802. ['pricingPlans[].durationDays', 'Integer', '会员有效天数(30/365)'],
  803. ['pricingPlans[].description', 'String', '方案描述']
  804. ],
  805. 'platformAccount': [
  806. ['id', 'Long', '记录ID'],
  807. ['userId', 'Long', '用户ID'],
  808. ['platformCode', 'String', '平台代码'],
  809. ['platformName', 'String', '平台名称'],
  810. ['accountEncrypted', 'String', '加密账号'],
  811. ['passwordEncrypted', 'String', '加密密码'],
  812. ['enabled', 'Boolean', '是否启用'],
  813. ['lastVerifyTime', 'DateTime', '最后验证时间'],
  814. ['verifyStatus', 'String', '验证状态(VERIFIED/FAILED/PENDING)'],
  815. ['failReason', 'String', '失败原因'],
  816. ['createTime', 'DateTime', '创建时间'],
  817. ['updateTime', 'DateTime', '更新时间']
  818. ],
  819. 'businessLicense': [
  820. ['id', 'Long', '记录ID'],
  821. ['userId', 'Long', '用户ID'],
  822. ['licenseImageUrl', 'String', '执照图片URL'],
  823. ['creditCode', 'String', '统一社会信用代码'],
  824. ['pharmacyName', 'String', '药店名称'],
  825. ['legalPerson', 'String', '法人代表'],
  826. ['registeredAddress', 'String', '注册地址'],
  827. ['businessScope', 'String', '经营范围'],
  828. ['validFrom', 'Date', '有效期开始'],
  829. ['validTo', 'Date', '有效期结束'],
  830. ['reviewStatus', 'String', '审核状态(PENDING/APPROVED/REJECTED)'],
  831. ['reviewerId', 'Long', '审核人ID'],
  832. ['reviewTime', 'DateTime', '审核时间'],
  833. ['rejectReason', 'String', '驳回原因'],
  834. ['showVerifiedBadge', 'Boolean', '是否展示认证徽章'],
  835. ['remark', 'String', '备注'],
  836. ['createTime', 'DateTime', '创建时间'],
  837. ['updateTime', 'DateTime', '更新时间']
  838. ],
  839. 'platformConfig': [
  840. ['id', 'Long', '配置ID'],
  841. ['platformCode', 'String', '平台代码(如yaoshibang)'],
  842. ['platformName', 'String', '平台名称'],
  843. ['enabled', 'Boolean', '是否启用'],
  844. ['requiresRecharge', 'Boolean', '是否需要充值'],
  845. ['queryCost', 'Integer', '每次查询消耗'],
  846. ['priority', 'Integer', '优先级'],
  847. ['underMaintenance', 'Boolean', '是否维护中'],
  848. ['maintenanceNotice', 'String', '维护公告'],
  849. ['description', 'String', '平台描述'],
  850. ['officialUrl', 'String', '官方网址'],
  851. ['bindingInstructions', 'String', '绑定说明'],
  852. ['sortOrder', 'Integer', '排序'],
  853. ['createTime', 'DateTime', '创建时间'],
  854. ['updateTime', 'DateTime', '更新时间'],
  855. ['createdBy', 'String', '创建人'],
  856. ['updatedBy', 'String', '更新人']
  857. ],
  858. 'trialQuota': [
  859. ['id', 'Long', '配置ID'],
  860. ['configName', 'String', '配置名称'],
  861. ['trialDays', 'Integer', '体验天数'],
  862. ['dailyQueryLimit', 'Integer', '每日查询限制'],
  863. ['monthlyWatchlistLimit', 'Integer', '月关注限制'],
  864. ['enabled', 'Boolean', '是否启用'],
  865. ['configType', 'String', '配置类型(DEFAULT/CUSTOM)'],
  866. ['applicableUserType', 'String', '适用用户类型(NEW_USER/ALL)'],
  867. ['effectiveFrom', 'DateTime', '生效开始时间'],
  868. ['effectiveTo', 'DateTime', '生效结束时间'],
  869. ['maxUsers', 'Integer', '最大用户数'],
  870. ['currentUsers', 'Integer', '当前用户数'],
  871. ['priority', 'Integer', '优先级'],
  872. ['description', 'String', '描述'],
  873. ['remark', 'String', '备注'],
  874. ['createdBy', 'String', '创建人'],
  875. ['updatedBy', 'String', '更新人'],
  876. ['createTime', 'DateTime', '创建时间'],
  877. ['updateTime', 'DateTime', '更新时间']
  878. ],
  879. 'inviteConfig': [
  880. ['id', 'Long', '配置ID'],
  881. ['rewardCrawlerCount', 'Integer', '每次邀请奖励爬虫次数(历史字段)'],
  882. ['rewardType', 'String', '奖励类型(MEMBERSHIP等)'],
  883. ['rewardMonths', 'Integer', '邀请人奖励会员时长(月)'],
  884. ['maxDailyReward', 'Integer', '每日最大奖励次数'],
  885. ['maxTotalInvites', 'Integer', '单个账号总邀请上限(默认30)'],
  886. ['inviteCodeExpireDays', 'Integer', '邀请码过期天数'],
  887. ['maxInvitePerDay', 'Integer', '每日最大邀请数'],
  888. ['appDownloadUrl', 'String', 'APP下载地址'],
  889. ['miniappPath', 'String', '小程序路径'],
  890. ['miniappAppId', 'String', '小程序AppID'],
  891. ['wechatRedirectUrl', 'String', '微信跳转URL'],
  892. ['dingtalkAppId', 'String', '钉钉AppID'],
  893. ['feishuAppId', 'String', '飞书AppID'],
  894. ['landingTitle', 'String', '落地页标题'],
  895. ['landingDesc', 'String', '落地页描述'],
  896. ['appName', 'String', '应用名称'],
  897. ['status', 'String', '状态(ACTIVE/INACTIVE)'],
  898. ['remark', 'String', '备注'],
  899. ['createTime', 'DateTime', '创建时间'],
  900. ['updateTime', 'DateTime', '更新时间']
  901. ],
  902. 'quotaGrant': [
  903. ['id', 'Long', '发放记录ID'],
  904. ['userId', 'Long', '用户ID'],
  905. ['grantType', 'String', '来源类型(ADMIN/INVITE/PURCHASE)'],
  906. ['quotaCount', 'Integer', '发放配额数'],
  907. ['usedCount', 'Integer', '已使用数'],
  908. ['expireTime', 'DateTime', '过期时间'],
  909. ['sourceUserId', 'Long', '来源用户ID(邀请人)'],
  910. ['remark', 'String', '备注'],
  911. ['status', 'String', '状态(ACTIVE/EXPIRED/USED_UP)'],
  912. ['createTime', 'DateTime', '创建时间'],
  913. ['updateTime', 'DateTime', '更新时间']
  914. ],
  915. 'crawlerUserDetail': [
  916. ['userId', 'Long', '用户ID'],
  917. ['nickname', 'String', '昵称'],
  918. ['phone', 'String', '手机号'],
  919. ['todayTotalUsage', 'Integer', '今日总使用次数'],
  920. ['monthlyTotalUsage', 'Integer', '本月总使用次数'],
  921. ['platformStats', 'Array', '各平台使用统计(见下方)'],
  922. ['platformStats[].platformCode', 'String', '平台代码'],
  923. ['platformStats[].platformName', 'String', '平台名称'],
  924. ['platformStats[].todayUsage', 'Integer', '今日使用'],
  925. ['platformStats[].monthlyUsage', 'Integer', '本月使用'],
  926. ['platformStats[].totalUsage', 'Integer', '累计使用']
  927. ],
  928. 'conversionStats': [
  929. ['inviteCode', 'String', '邀请码'],
  930. ['clickedCount', 'Integer', '链接点击次数'],
  931. ['totalInvited', 'Integer', '总邀请数'],
  932. ['registeredCount', 'Integer', '注册人数'],
  933. ['pendingCount', 'Integer', '待审核数'],
  934. ['conversionRate', 'Double', '转化率(%)'],
  935. ['totalReward', 'Integer', '总奖励数']
  936. ],
  937. 'auditLog': [
  938. ['id', 'Long', '日志ID'],
  939. ['operatorId', 'Long', '操作人ID'],
  940. ['operatorName', 'String', '操作人姓名'],
  941. ['operatorRole', 'String', '操作人角色'],
  942. ['operationTime', 'DateTime', '操作时间'],
  943. ['ipAddress', 'String', '操作IP'],
  944. ['userAgent', 'String', '浏览器UA'],
  945. ['module', 'String', '模块(USER/INVITE/CRAWLER等)'],
  946. ['operationType', 'String', '操作类型(CREATE/UPDATE/DELETE等)'],
  947. ['description', 'String', '操作描述'],
  948. ['targetType', 'String', '目标类型'],
  949. ['targetId', 'Long', '目标ID'],
  950. ['targetIdentifier', 'String', '目标标识'],
  951. ['beforeData', 'Object', '变更前数据'],
  952. ['afterData', 'Object', '变更后数据'],
  953. ['result', 'String', '操作结果(SUCCESS/FAILED)'],
  954. ['errorMessage', 'String', '错误信息'],
  955. ['requestUrl', 'String', '请求URL'],
  956. ['requestMethod', 'String', '请求方法'],
  957. ['requestParams', 'String', '请求参数'],
  958. ['responseCode', 'Integer', '响应码'],
  959. ['duration', 'Long', '耗时(ms)'],
  960. ['remark', 'String', '备注'],
  961. ['isSensitive', 'Boolean', '是否敏感操作'],
  962. ['createTime', 'DateTime', '创建时间']
  963. ],
  964. 'inviteLinkResolve': [
  965. ['inviteCode', 'String', '邀请码'],
  966. ['inviteLink', 'String', '完整邀请链接'],
  967. ['inviterNickname', 'String', '邀请人昵称'],
  968. ['inviterAvatar', 'String', '邀请人头像'],
  969. ['appName', 'String', '应用名称'],
  970. ['landingTitle', 'String', '落地页标题'],
  971. ['landingDesc', 'String', '落地页描述']
  972. ],
  973. };
  974. function renderFieldDesc(descKey) {
  975. const fields = FIELD_DESC[descKey];
  976. if (!fields) return '';
  977. let rows = fields.map(f => `<tr><td><code>${f[0]}</code></td><td>${f[1]}</td><td>${f[2]}</td></tr>`).join('');
  978. return `<div class="field-desc-box">
  979. <div class="field-desc-title">返回字段说明</div>
  980. <table class="field-desc-table"><tr><th>字段名</th><th>类型</th><th>说明</th></tr>${rows}</table>
  981. </div>`;
  982. }
  983. // ==================== 各模块HTML ====================
  984. function testSection(id, title, method, methodClass, path, authTag, authClass, fields, hasValidator, descKey, desc) {
  985. const authLabel = authTag === '需认证' ? '需认证' : '公开';
  986. const ac = authTag === '需认证' ? 'auth-required' : 'auth-public';
  987. const side = (path.includes('/api/admin') || path === '/api/coupon/issue' || path.includes('/api/level/user/')) ? 'admin' : 'user';
  988. const sideClass = side === 'admin' ? 'side-admin' : 'side-user';
  989. const sideLabel = side === 'admin' ? '运营侧' : '用户侧';
  990. let fieldsHtml = '';
  991. if (fields) {
  992. fieldsHtml = '<div class="form-grid">';
  993. fields.forEach(f => {
  994. fieldsHtml += `<div class="form-group"><label>${f.label}</label><input type="text" id="${id}-${f.name}" placeholder="${f.placeholder || ''}" value="${f.default || ''}"></div>`;
  995. });
  996. fieldsHtml += '</div>';
  997. }
  998. const descHtml = desc ? `<div class="section-desc">${desc}</div>` : '';
  999. const fieldDescHtml = descKey ? renderFieldDesc(descKey) : '';
  1000. return `
  1001. <div class="test-section">
  1002. <div class="section-title">
  1003. <span class="method method-${methodClass}">${method}</span>
  1004. <span class="path">${path}</span>
  1005. <span class="side-tag ${sideClass}">${sideLabel}</span>
  1006. <span class="${ac} auth-tag">${authLabel}</span>
  1007. </div>
  1008. ${descHtml}
  1009. ${fieldsHtml}
  1010. <button class="btn btn-primary" onclick="runTest('${id}', '${method}', '${path}', ${buildBody(id, fields)}, ${authTag === '公开' ? 'false' : 'true'}, ${hasValidator ? `'${toValidatorName(id)}'` : 'null'})">执行测试</button>
  1011. <div class="response-box" id="${id}-box" style="display:none;">
  1012. <div class="response-header">
  1013. <span class="response-title">响应 <span id="${id}-duration" style="color:#999;font-weight:400;"></span></span>
  1014. <span class="status-badge" id="${id}-status"></span>
  1015. </div>
  1016. <div class="response-content" id="${id}-raw"></div>
  1017. ${fieldDescHtml}
  1018. </div>
  1019. </div>`;
  1020. }
  1021. function buildBody(id, fields) {
  1022. if (!fields || fields.length === 0) return 'null';
  1023. let parts = [];
  1024. fields.forEach(f => {
  1025. const forceArray = f.type === 'array';
  1026. const asJson = f.type === 'json';
  1027. if (asJson) {
  1028. parts.push(`&quot;${f.name}&quot;: safeJsonParse(document.getElementById('${id}-${f.name}').value)`);
  1029. } else {
  1030. parts.push(`&quot;${f.name}&quot;: parseFieldValue(document.getElementById('${id}-${f.name}').value, ${forceArray})`);
  1031. }
  1032. });
  1033. return `{${parts.join(',')}}`;
  1034. }
  1035. /**
  1036. * 智能解析字段值:返回真实类型的JS值(非源代码字符串)
  1037. * 逗号分隔→数组,true/false→布尔,数字(含小数)→数值,否则→字符串
  1038. * @param forceArray 为true时,单个值也包装为数组(如userIds填1→[1])
  1039. */
  1040. function parseFieldValue(val, forceArray) {
  1041. if (val === '' || val === undefined || val === null) return forceArray ? [] : null;
  1042. // 布尔
  1043. if (val === 'true') return forceArray ? [true] : true;
  1044. if (val === 'false') return forceArray ? [false] : false;
  1045. // 逗号分隔的数组
  1046. if (val.includes(',')) {
  1047. const items = val.split(',').map(v => v.trim()).filter(v => v !== '');
  1048. return items.map(v => toNumberIfSafe(v));
  1049. }
  1050. // 数字(整数或小数,如 1, 9.90, 0.5);大整数(雪花ID)保持字符串
  1051. if (/^\d+(\.\d+)?$/.test(val)) return forceArray ? [toNumberIfSafe(val)] : toNumberIfSafe(val);
  1052. // 字符串
  1053. return forceArray ? [val] : val;
  1054. }
  1055. /**
  1056. * 尝试转为Number,但大整数(超出JS安全范围如雪花ID)保持字符串避免精度丢失
  1057. */
  1058. function toNumberIfSafe(v) {
  1059. if (!/^\d+(\.\d+)?$/.test(v)) return v;
  1060. const num = Number(v);
  1061. // 整数且超出安全范围(如雪花ID 2069620495151230978) → 保持字符串
  1062. if (Number.isInteger(num) && !Number.isSafeInteger(num)) return v;
  1063. return num;
  1064. }
  1065. function renderAuthModule() {
  1066. return `
  1067. <div class="batch-buttons">
  1068. <button class="btn btn-warning btn-sm" onclick="runAuthTests()">运行全部认证模块测试</button>
  1069. </div>
  1070. ${testSection('auth-send', '发送验证码', 'POST', 'post', '/api/auth/sms/send', '公开', 'auth-public',
  1071. [{label:'手机号(phone)', name:'phone', placeholder:'18776156917', default:'18776156917'},
  1072. {label:'场景(scene)', name:'scene', placeholder:'login', default:'login'}], true, null, '发送短信验证码,开发环境可能不实际发送')}
  1073. ${testSection('auth-smslogin', '短信登录', 'POST', 'post', '/api/auth/sms/login', '公开', 'auth-public',
  1074. [{label:'手机号(phone)', name:'phone', placeholder:'18776156917', default:'18776156917'},
  1075. {label:'验证码(code)', name:'code', placeholder:'123456', default:'123456'},
  1076. {label:'邀请码(inviteCode,可选)', name:'inviteCode', placeholder:'ABC123', default:''},
  1077. {label:'场景(scene)', name:'scene', placeholder:'login', default:'login'}], true, 'loginResponse', '验证码登录/注册,支持附带邀请码自动绑定邀请关系')}
  1078. ${testSection('auth-pwdlogin', '密码登录', 'POST', 'post', '/api/auth/password/login', '公开', 'auth-public',
  1079. [{label:'手机号(phone)', name:'phone', placeholder:'18776156917', default:'18776156917'},
  1080. {label:'密码(password)', name:'password', placeholder:'your_password', default:'123456'}], true, 'loginResponse', '使用手机号+密码登录,需先通过设置密码接口创建密码')}
  1081. ${testSection('auth-wechat-url', '微信扫码URL', 'GET', 'get', '/api/auth/wechat/url?state=login', '公开', 'auth-public', null, false, null, '获取微信扫码登录跳转URL')}
  1082. ${testSection('auth-wechat-login', '微信登录', 'POST', 'post', '/api/auth/wechat/login', '公开', 'auth-public',
  1083. [{label:'微信code(code)', name:'code', placeholder:'微信授权code', default:'test_wechat_code'},
  1084. {label:'state(state)', name:'state', placeholder:'login', default:'login'}], true, 'loginResponse', '微信扫码登录后回调,传入微信授权code')}
  1085. ${testSection('auth-wechat-bind', '微信绑定手机', 'POST', 'post', '/api/auth/wechat/bind', '需认证', 'auth-required',
  1086. [{label:'手机号(phone)', name:'phone', placeholder:'18776156917', default:'18776156917'},
  1087. {label:'验证码(code)', name:'code', placeholder:'123456', default:'123456'},
  1088. {label:'临时令牌(tempToken)', name:'tempToken', placeholder:'微信登录后获取的tempToken', default:''},
  1089. {label:'邀请码(inviteCode,可选)', name:'inviteCode', placeholder:'ABC123', default:''}], true, 'loginResponse', '已有微信登录的用户绑定手机号')}
  1090. ${testSection('auth-wechat-callback', '微信回调', 'GET', 'get', '/api/auth/wechat/callback?code=test_code', '公开', 'auth-public', null, false, null, '微信扫码登录前端跳转回调')}
  1091. ${testSection('auth-refresh', '刷新Token', 'POST', 'post', '/api/auth/refresh', '公开', 'auth-public',
  1092. [{label:'refreshToken(refreshToken)', name:'refreshToken', placeholder:'输入refreshToken', default:''}], false, 'loginResponse', '使用refreshToken获取新的accessToken')}
  1093. ${testSection('auth-userinfo', '获取用户信息', 'GET', 'get', '/api/auth/user/info', '需认证', 'auth-required', null, true, 'userInfo', '获取当前登录用户的基本信息')}
  1094. ${testSection('auth-setpwd', '设置密码', 'POST', 'post', '/api/auth/password/set', '需认证', 'auth-required',
  1095. [{label:'旧密码(oldPassword,可选)', name:'oldPassword', placeholder:'首次设置可不填', default:''},
  1096. {label:'新密码(newPassword)', name:'newPassword', placeholder:'new_password', default:'Abc123456'}], false, null, '首次设置密码或修改密码')}
  1097. ${testSection('auth-change-phone', '修改手机号', 'POST', 'post', '/api/auth/phone/change', '需认证', 'auth-required',
  1098. [{label:'新手机号(newPhone)', name:'newPhone', placeholder:'13900139000', default:'13900139000'},
  1099. {label:'验证码(code)', name:'code', placeholder:'123456', default:'123456'}], false, null, '修改绑定手机号,需先点击发送验证码获取新手机验证码')}
  1100. <div style="margin-top:-8px;margin-bottom:14px;">
  1101. <button class="btn btn-primary btn-sm" onclick="sendCodeToField('auth-change-phone-newPhone','change_phone')">发送验证码到新手机</button>
  1102. <span style="font-size:11px;color:#888;margin-left:8px;">开发环境固定验证码: 123456</span>
  1103. </div>`;
  1104. }
  1105. function renderCrawlerModule() {
  1106. return `
  1107. <div class="batch-buttons">
  1108. <button class="btn btn-warning btn-sm" onclick="runCrawlerTests()">运行全部爬虫配额测试</button>
  1109. </div>
  1110. ${testSection('crawler-status', '配额状态', 'GET', 'get', '/api/crawler/status', '需认证', 'auth-required', null, true, 'crawlerStatus', '获取当前用户的爬虫配额使用情况')}
  1111. ${testSection('crawler-consume', '消耗配额', 'POST', 'post', '/api/crawler/consume', '需认证', 'auth-required',
  1112. [{label:'次数(count)', name:'count', placeholder:'1', default:'1'},
  1113. {label:'优先优惠券(useCouponFirst)', name:'useCouponFirst', placeholder:'true/false', default:'false'},
  1114. {label:'平台代码(platform)', name:'platform', placeholder:'yaoshibang', default:'yaoshibang'}], true, null, '前端启动爬虫前调用,扣减配额次数')}
  1115. ${testSection('crawler-logs', '使用记录', 'GET', 'get', '/api/crawler/logs?days=7', '需认证', 'auth-required', null, true, 'crawlerLog', '查询爬虫使用历史记录')}`;
  1116. }
  1117. function renderInviteModule() {
  1118. return `
  1119. <div class="batch-buttons">
  1120. <button class="btn btn-warning btn-sm" onclick="runInviteTests()">运行全部邀请系统测试</button>
  1121. </div>
  1122. ${testSection('invite-code', '获取邀请码', 'GET', 'get', '/api/invite/code', '需认证', 'auth-required', null, true, 'inviteCode', '获取当前用户的邀请码和邀请链接')}
  1123. ${testSection('invite-stats', '邀请统计', 'GET', 'get', '/api/invite/stats', '需认证', 'auth-required', null, true, 'inviteStats', '获取邀请人数、注册人数、累计奖励等统计')}
  1124. ${testSection('invite-rewards', '邀请记录', 'GET', 'get', '/api/invite/rewards', '需认证', 'auth-required', null, true, 'inviteReward', '获取邀请记录列表,含被邀请人注册状态')}
  1125. ${testSection('invite-extra-quota', '额外配额', 'GET', 'get', '/api/invite/extra-quota', '需认证', 'auth-required', null, false, null, '获取可用额外爬虫次数(邀请奖励+运营发放),返回Integer数值')}
  1126. ${testSection('invite-bind', '补填邀请码', 'POST', 'post', '/api/invite/bind', '需认证', 'auth-required',
  1127. [{label:'邀请码/链接(inviteCode)', name:'inviteCode', placeholder:'A3K7M9P2 或 https://app.zhijiayun.com/invite/A3K7M9P2', default:''}], true, null, '注册时未填邀请码,注册后30天内可补填。补填后邀请人获赠1个月高级会员时长')}
  1128. ${testSection('invite-page', '落地页', 'GET', 'get', '/api/invite/page/{code}?channel=windows', '公开', 'auth-public',
  1129. [{label:'邀请码(code)', name:'code', placeholder:'输入邀请码', default:''},
  1130. {label:'渠道(channel)', name:'channel', placeholder:'windows/app/wechat/miniapp/dingtalk/feishu', default:'windows'}], true, 'invitePage', '浏览器打开邀请链接时展示的落地页数据,默认渠道为windows(Windows桌面客户端)')}
  1131. ${testSection('invite-link-entry', '邀请链接入口', 'GET', 'get', '/api/invite/{code}?channel=windows', '公开', 'auth-public',
  1132. [{label:'邀请码(code)', name:'code', placeholder:'输入邀请码', default:''}], true, 'invitePage', '浏览器直接访问邀请链接入口,默认windows渠道,自动计入点击数')}
  1133. ${testSection('invite-click', '点击追踪', 'POST', 'post', '/api/invite/click/{code}', '公开', 'auth-public',
  1134. [{label:'邀请码(code)', name:'code', placeholder:'输入邀请码', default:''}], false, null, '追踪邀请链接被打开事件')}
  1135. ${testSection('invite-resolve', '解析链接', 'GET', 'get', '/api/invite/resolve-link?link=', '公开', 'auth-public',
  1136. [{label:'链接/邀请码(link)', name:'link', placeholder:'https://app.zhijiayun.com/invite/ABC123 或 ABC123', default:''}], false, 'inviteLinkResolve', '解析邀请链接或纯邀请码,识别出邀请人和邀请码')}`;
  1137. }
  1138. function renderCouponModule() {
  1139. return `
  1140. <div class="batch-buttons">
  1141. <button class="btn btn-warning btn-sm" onclick="runCouponTests()">运行全部优惠券测试</button>
  1142. </div>
  1143. ${testSection('coupon-my', '我的优惠券', 'GET', 'get', '/api/coupon/my?status=0', '需认证', 'auth-required', null, true, 'coupon', '查询当前用户的优惠券列表,可按状态筛选')}
  1144. ${testSection('coupon-available', '可用优惠券', 'GET', 'get', '/api/coupon/my/available?type=CRAWLER', '需认证', 'auth-required', null, true, 'coupon', '查询当前用户可用的优惠券,可按类型筛选')}`;
  1145. }
  1146. function renderMembershipModule() {
  1147. return `
  1148. <div class="batch-buttons">
  1149. <button class="btn btn-warning btn-sm" onclick="runMembershipTests()">运行全部会员体系测试</button>
  1150. </div>
  1151. ${testSection('membership-my', '我的会员', 'GET', 'get', '/api/membership/my', '需认证', 'auth-required', null, true, 'membership', '获取当前登录用户的会员等级与有效期')}
  1152. ${testSection('membership-benefits', '会员权益', 'GET', 'get', '/api/membership/benefits?level=PLUS', '需认证', 'auth-required',
  1153. [{label:'等级(level)', name:'level', placeholder:'PLUS/PRO/ULTRA', default:'PLUS'}], true, 'membershipBenefit', '查询指定会员等级的权益列表')}`;
  1154. }
  1155. function renderPaymentModule() {
  1156. return `
  1157. <div class="batch-buttons">
  1158. <button class="btn btn-warning btn-sm" onclick="runPaymentTests()">运行全部支付模块测试</button>
  1159. </div>
  1160. ${testSection('payment-plans', '支付方案列表', 'GET', 'get', '/api/payment/plans', '公开', 'auth-public', null, true, null, '获取所有可用的支付方案列表(高级会员月卡/年卡)')}
  1161. ${testSection('payment-create-order', '创建支付订单', 'POST', 'post', '/api/payment/order/create', '需认证', 'auth-required',
  1162. [{label:'方案ID(planId)', name:'planId', placeholder:'1', default:'1'},
  1163. {label:'支付渠道(channel)', name:'channel', placeholder:'WECHAT/ALIPAY', default:'WECHAT'}], true, null, '创建支付订单,微信返回二维码(qrCodeBase64)供扫码支付,支付宝通过alipay.trade.precreate返回qr_code,前端自定义弹窗展示二维码并自动轮询支付状态。15分钟内未付款订单自动过期')}
  1164. ${testSection('payment-query-order', '查询订单', 'GET', 'get', '/api/payment/order/{orderNo}', '需认证', 'auth-required',
  1165. [{label:'订单号(orderNo)', name:'orderNo', placeholder:'OP...', default:''}], true, null, '根据订单号查询支付订单详情,响应中expireHint含过期提示')}
  1166. ${testSection('payment-orders', '我的订单列表', 'GET', 'get', '/api/payment/orders', '需认证', 'auth-required', null, true, null, '查询当前用户的所有支付订单,待支付订单含expireHint过期提示')}
  1167. ${testSection('payment-mock-pay', '手动确认支付', 'POST', 'post', '/api/payment/order/{orderNo}/pay', '需认证', 'auth-required',
  1168. [{label:'订单号(orderNo)', name:'orderNo', placeholder:'OP...', default:''}], false, null, '开发测试用。mock模式=模拟支付自动激活会员;真实支付模式=手动确认支付(不实际扣款,用于测试回调流程)。需在15分钟过期前操作')}
  1169. ${testSection('payment-callback-wechat', '微信回调', 'POST', 'post', '/api/payment/callback/wechat', '公开', 'auth-public',
  1170. [{label:'订单号(out_trade_no)', name:'out_trade_no', placeholder:'OP...', default:''},
  1171. {label:'交易号(transaction_id,可选)', name:'transaction_id', placeholder:'wx_', default:''}], false, null, '微信支付回调通知。mock模式从参数取订单号,真实支付模式需验签+解密(API v3)')}
  1172. ${testSection('payment-callback-alipay', '支付宝回调', 'POST', 'post', '/api/payment/callback/alipay', '公开', 'auth-public',
  1173. [{label:'订单号(out_trade_no)', name:'out_trade_no', placeholder:'OP...', default:''},
  1174. {label:'交易号(trade_no,可选)', name:'trade_no', placeholder:'2024...', default:''}], false, null, '支付宝回调通知。mock模式从参数取订单号,真实支付模式需RSA2验签')}`;
  1175. }
  1176. function renderUploadModule() {
  1177. return `
  1178. <div class="batch-buttons">
  1179. <button class="btn btn-warning btn-sm" onclick="runUploadTests()">运行全部上传测试</button>
  1180. </div>
  1181. <div class="test-section">
  1182. <div class="section-title">
  1183. <span class="method method-post">POST</span>
  1184. <span class="path">/api/upload</span>
  1185. <span class="side-tag side-user">用户侧</span>
  1186. <span class="auth-required auth-tag">需认证</span>
  1187. </div>
  1188. <div class="section-desc">上传单个文件,返回文件访问路径。使用 multipart/form-data 格式,字段名: file</div>
  1189. <div class="form-grid">
  1190. <div class="form-group">
  1191. <label>选择文件</label>
  1192. <input type="file" id="upload-file" accept="image/*,.pdf,.doc,.docx" style="padding:8px;">
  1193. </div>
  1194. </div>
  1195. <button class="btn btn-primary" onclick="uploadFile()">上传文件</button>
  1196. <div class="response-box" id="upload-result-box" style="display:none;">
  1197. <div class="response-header">
  1198. <span class="response-title">响应 <span id="upload-duration" style="color:#999;font-weight:400;"></span></span>
  1199. <span class="status-badge" id="upload-status"></span>
  1200. </div>
  1201. <div class="response-content" id="upload-raw"></div>
  1202. </div>
  1203. </div>`;
  1204. }
  1205. function renderSearchModule() {
  1206. return `
  1207. <div class="batch-buttons">
  1208. <button class="btn btn-warning btn-sm" onclick="runSearchTests()">运行全部搜索测试</button>
  1209. </div>
  1210. ${testSection('search-query', '药品搜索', 'POST', 'post', '/api/search/query', '需认证', 'auth-required',
  1211. [{label:'关键词(keyword)', name:'keyword', placeholder:'阿莫西林胶囊', default:'阿莫西林胶囊'},
  1212. {label:'平台列表(platforms,可选)', name:'platforms', placeholder:'yaoshibang,yaobangmang,yiyaocheng', default:'', type:'array'}], true, null, '搜索药品比价,消耗每日配额,返回各平台价格列表。platforms传多个平台代码用逗号分隔,不传则查询全部平台')}
  1213. ${testSection('search-suggestions', '搜索建议', 'GET', 'get', '/api/search/suggestions', '公开', 'auth-public', null, true, null, '获取热门药品搜索建议列表(无需认证)')}`;
  1214. }
  1215. function renderPlatformAccountModule() {
  1216. return `
  1217. <div class="batch-buttons">
  1218. <button class="btn btn-warning btn-sm" onclick="runPlatformAccountTests()">运行全部平台账号测试</button>
  1219. </div>
  1220. ${testSection('pa-bind', '绑定平台账号', 'POST', 'post', '/api/platform-account/bind', '需认证', 'auth-required',
  1221. [{label:'平台代码(platformCode)', name:'platformCode', placeholder:'yaoshibang', default:'yaoshibang'},
  1222. {label:'平台名称(platformName)', name:'platformName', placeholder:'药师帮', default:'药师帮'},
  1223. {label:'账号(account)', name:'account', placeholder:'18776156917', default:'18776156917'},
  1224. {label:'密码(password)', name:'password', placeholder:'your_password', default:'Abc123456'}], false, null, '绑定B2B药品平台账号')}
  1225. ${testSection('pa-list', '已绑定列表', 'GET', 'get', '/api/platform-account/list', '需认证', 'auth-required', null, true, 'platformAccount', '查询已绑定的所有平台账号')}
  1226. ${testSection('pa-verify', '验证账号', 'POST', 'post', '/api/platform-account/verify/{platformCode}', '需认证', 'auth-required',
  1227. [{label:'平台代码(platformCode)', name:'platformCode', placeholder:'yaoshibang', default:'yaoshibang'}], false, null, '验证已绑定的平台账号是否有效')}
  1228. ${testSection('pa-unbind', '解绑账号', 'DELETE', 'delete', '/api/platform-account/unbind/{platformCode}', '需认证', 'auth-required',
  1229. [{label:'平台代码(platformCode)', name:'platformCode', placeholder:'yaoshibang', default:'yaoshibang'}], false, null, '解绑指定平台账号')}`;
  1230. }
  1231. function renderBusinessLicenseModule() {
  1232. return `
  1233. <div class="batch-buttons">
  1234. <button class="btn btn-warning btn-sm" onclick="runBusinessLicenseTests()">运行全部营业执照测试</button>
  1235. </div>
  1236. ${testSection('bl-upload', '上传执照(JSON)', 'POST', 'post', '/api/business-license/upload', '需认证', 'auth-required',
  1237. [{label:'图片URL(licenseImageUrl)', name:'licenseImageUrl', placeholder:'https://oss.example.com/license/xxx.jpg', default:'https://oss.example.com/license/test.jpg'},
  1238. {label:'信用代码(creditCode)', name:'creditCode', placeholder:'91110000XXXXXXXXXX', default:'91110000TEST000001'},
  1239. {label:'药店名称(pharmacyName)', name:'pharmacyName', placeholder:'XX大药房', default:'测试大药房'},
  1240. {label:'法人(legalPerson,可选)', name:'legalPerson', placeholder:'张三', default:'张三'}], false, null, '通过JSON方式上传营业执照(传图片URL)')}
  1241. ${testSection('bl-info', '查询执照信息', 'GET', 'get', '/api/business-license/info', '需认证', 'auth-required', null, true, 'businessLicense', '查询当前用户已上传的营业执照信息')}`;
  1242. }
  1243. function renderPlatformConfigModule() {
  1244. return `
  1245. <div class="batch-buttons">
  1246. <button class="btn btn-warning btn-sm" onclick="runPlatformConfigTests()">运行全部平台配置测试</button>
  1247. </div>
  1248. ${testSection('pc-enabled', '启用平台列表', 'GET', 'get', '/api/platform-config/enabled', '公开', 'auth-public', null, true, 'platformConfig', '获取所有已启用的爬虫平台配置列表')}
  1249. ${testSection('pc-detail', '平台详情', 'GET', 'get', '/api/platform-config/{platformCode}', '公开', 'auth-public',
  1250. [{label:'平台代码(platformCode)', name:'platformCode', placeholder:'yaoshibang', default:'yaoshibang'}], true, 'platformConfig', '获取指定平台的详细配置信息')}`;
  1251. }
  1252. function renderSearchRecordModule() {
  1253. return `
  1254. <div class="batch-buttons">
  1255. <button class="btn btn-warning btn-sm" onclick="runSearchRecordTests()">运行全部搜索记录测试</button>
  1256. </div>
  1257. ${testSection('sr-save', '保存搜索记录', 'POST', 'post', '/api/search-record/history', '需认证', 'auth-required',
  1258. [{label:'药品名称(drugName)', name:'drugName', placeholder:'阿莫西林', default:'阿莫西林'},
  1259. {label:'搜索时间(searchedAt)', name:'searchedAt', placeholder:'2026-06-26T08:30:00.000Z', default:'2026-06-26T08:30:00.000Z'},
  1260. {label:'状态文案(statusText)', name:'statusText', placeholder:'已返回 2 个平台的可比价结果。', default:'已返回 2 个平台的可比价结果。'},
  1261. {label:'平台结果(platformResults)', name:'platformResults', placeholder:'[{"platformId":"yiyaocheng","platformName":"壹药城"}]', default:'[{"platformId":"yiyaocheng","platformName":"壹药城","response":{"status":"ok","products":[]}}]', type:'json'},
  1262. {label:'搜索日志(searchLog)', name:'searchLog', placeholder:'null', default:'null', type:'json'}], true, null, '保存一次完整的比价搜索快照,包含平台响应数据。platformResults和searchLog支持任意JSON结构')}
  1263. ${testSection('sr-list', '搜索记录列表', 'GET', 'get', '/api/search-record/history?limit={limit}', '需认证', 'auth-required',
  1264. [{label:'条数(limit)', name:'limit', placeholder:'20', default:'20'}], true, null, '获取当前用户最近的搜索记录,默认20条,最多50条')}
  1265. ${testSection('sr-detail', '记录详情', 'GET', 'get', '/api/search-record/history/{id}', '需认证', 'auth-required',
  1266. [{label:'记录ID(id)', name:'id', placeholder:'1', default:'1'}], true, null, '读取单条搜索记录详情(包含完整的platformResults)')}
  1267. ${testSection('sr-delete', '删除记录', 'DELETE', 'delete', '/api/search-record/history/{id}', '需认证', 'auth-required',
  1268. [{label:'记录ID(id)', name:'id', placeholder:'1', default:'1'}], false, null, '删除单条搜索记录(幂等,不存在也返回成功)')}
  1269. ${testSection('sr-clear', '清空记录', 'DELETE', 'delete', '/api/search-record/history', '需认证', 'auth-required', null, false, null, '清空当前用户全部搜索记录')}`;
  1270. }
  1271. function renderPlatformSessionModule() {
  1272. return `
  1273. <div class="batch-buttons">
  1274. <button class="btn btn-warning btn-sm" onclick="runPlatformSessionTests()">运行全部平台会话测试</button>
  1275. </div>
  1276. ${testSection('ps-upload', '上传平台会话', 'PUT', 'put', '/api/platform-session/{platformCode}', '需认证', 'auth-required',
  1277. [{label:'平台代码(platformCode)', name:'platformCode', placeholder:'yiyaocheng', default:'yiyaocheng'},
  1278. {label:'平台名称(platformName)', name:'platformName', placeholder:'壹药城', default:'壹药城'},
  1279. {label:'账号标签(accountLabel)', name:'accountLabel', placeholder:'深圳大方无限科技有限公司', default:'测试账号'},
  1280. {label:'登录URL(loginUrl)', name:'loginUrl', placeholder:'https://mall.yaoex.com/login', default:'https://mall.yaoex.com/login'},
  1281. {label:'cookies(JSON数组)', name:'cookies', placeholder:'[{"name":"session","value":"xxx"}]', default:'[{"name":"mall_session","value":"test-cookie-value","domain":".mall.yaoex.com","path":"/","secure":true,"httpOnly":true,"expirationDate":1800000000}]', type:'json'}], true, null, '上传或覆盖当前用户某个平台的Cookie会话快照,Cookie会加密存储。platformCode必须与路径中的一致')}
  1282. ${testSection('ps-list', '平台会话列表', 'GET', 'get', '/api/platform-session/list', '需认证', 'auth-required', null, true, null, '获取当前用户所有已保存的平台会话(Cookie已解密),用于启动时恢复登录态')}
  1283. ${testSection('ps-delete', '删除平台会话', 'DELETE', 'delete', '/api/platform-session/{platformCode}', '需认证', 'auth-required',
  1284. [{label:'平台代码(platformCode)', name:'platformCode', placeholder:'yiyaocheng', default:'yiyaocheng'}], false, null, '删除某个平台的远端会话(幂等,不存在也返回成功)')}`;
  1285. }
  1286. // ==================== 运营侧模块 ====================
  1287. function renderAdminMembershipModule() {
  1288. return `
  1289. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminMembershipTests()">运行全部会员管理测试</button></div>
  1290. ${testSection('admin-membership-grant', '发放/调整会员', 'POST', 'post', '/api/admin/membership/grant', '需认证', 'auth-required',
  1291. [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'},
  1292. {label:'会员等级(level)', name:'level', placeholder:'PLUS/PRO/ULTRA', default:'PRO'},
  1293. {label:'来源(source)', name:'source', placeholder:'ADMIN_GRANT', default:'ADMIN_GRANT'},
  1294. {label:'备注(remark)', name:'remark', placeholder:'运营发放', default:'测试发放'}], false, 'membership', '运营/超管给用户发放或调整会员等级')}
  1295. ${testSection('admin-membership-list', '会员记录列表', 'GET', 'get', '/api/admin/membership/memberships?page=1&size=10', '需认证', 'auth-required', null, true, 'membership', '分页查询会员发放记录')}`;
  1296. }
  1297. function renderAdminInviteModule() {
  1298. return `
  1299. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminInviteTests()">运行全部邀请管理测试</button></div>
  1300. ${testSection('admin-invite-config', '邀请配置查询', 'GET', 'get', '/api/admin/invite/config', '需认证', 'auth-required', null, true, 'inviteConfig', '获取当前激活的邀请配置')}
  1301. ${testSection('admin-invite-configs', '全部邀请配置', 'GET', 'get', '/api/admin/invite/configs', '需认证', 'auth-required', null, true, 'inviteConfig', '获取所有邀请配置历史记录')}
  1302. ${testSection('admin-invite-config-update', '更新邀请配置', 'PUT', 'put', '/api/admin/invite/config', '需认证', 'auth-required',
  1303. [{label:'奖励月数(rewardMonths)', name:'rewardMonths', placeholder:'1', default:'1'},
  1304. {label:'总邀请上限(maxTotalInvites)', name:'maxTotalInvites', placeholder:'30', default:'30'},
  1305. {label:'日奖励上限(maxDailyReward)', name:'maxDailyReward', placeholder:'50', default:'50'},
  1306. {label:'邀请码过期天(inviteCodeExpireDays)', name:'inviteCodeExpireDays', placeholder:'30', default:'30'},
  1307. {label:'日邀请上限(maxInvitePerDay)', name:'maxInvitePerDay', placeholder:'10', default:'10'},
  1308. {label:'APP下载URL(appDownloadUrl)', name:'appDownloadUrl', placeholder:'https://...', default:'https://download.example.com/app.exe'},
  1309. {label:'落地页标题(landingTitle)', name:'landingTitle', placeholder:'标题', default:'邀请你加入智价云药店'}], false, 'inviteConfig', '更新邀请配置参数,rewardMonths=邀请人每次获赠高级会员月数,maxTotalInvites=单个账号总邀请上限')}
  1310. ${testSection('admin-invite-rewards', '邀请记录查询', 'GET', 'get', '/api/admin/invite/rewards?userId=1', '需认证', 'auth-required', null, true, 'inviteReward', '查看指定用户的邀请奖励记录')}
  1311. ${testSection('admin-invite-conversion', '邀请转化统计', 'GET', 'get', '/api/admin/invite/conversion-stats?userId=1', '需认证', 'auth-required', null, true, 'conversionStats', '查看用户邀请点击-注册转化率')}`;
  1312. }
  1313. function renderAdminCrawlerModule() {
  1314. return `
  1315. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminCrawlerTests()">运行全部爬虫管理测试</button></div>
  1316. ${testSection('admin-crawler-grant', '手动发放配额', 'POST', 'post', '/api/admin/crawler/grant', '需认证', 'auth-required',
  1317. [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'},
  1318. {label:'配额数(quotaCount)', name:'quotaCount', placeholder:'50', default:'10'},
  1319. {label:'过期天数(expireDays)', name:'expireDays', placeholder:'30', default:'30'},
  1320. {label:'备注(remark)', name:'remark', placeholder:'VIP升级奖励', default:'测试发放'}], false, 'quotaGrant', '运营手动给用户发放爬虫次数')}
  1321. ${testSection('admin-crawler-grants', '发放记录', 'GET', 'get', '/api/admin/crawler/grants', '需认证', 'auth-required', null, true, 'quotaGrant', '查询爬虫配额发放记录')}
  1322. ${testSection('admin-crawler-user-detail', '用户爬虫详情', 'GET', 'get', '/api/admin/crawler/user-detail?userId=1', '需认证', 'auth-required',
  1323. [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'}], true, 'crawlerUserDetail', '查看用户爬虫使用详情及各平台分布')}`;
  1324. }
  1325. function renderAdminCouponModule() {
  1326. return `
  1327. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminCouponTests()">运行全部优惠券发放测试</button></div>
  1328. ${testSection('admin-coupon-issue', '发放优惠券', 'POST', 'post', '/api/coupon/issue', '需认证', 'auth-required',
  1329. [{label:'模板ID(couponId)', name:'couponId', placeholder:'1', default:'1'},
  1330. {label:'用户ID列表(userIds)', name:'userIds', placeholder:'1,2,3', default:'1', type:'array'}], false, 'couponIssue', '管理员向指定用户发放优惠券,userIds统一用列表传入,单个用户填1即可')}`;
  1331. }
  1332. function renderAdminLicenseModule() {
  1333. return `
  1334. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminLicenseTests()">运行全部执照审核测试</button></div>
  1335. ${testSection('admin-license-list', '审核列表', 'GET', 'get', '/api/admin/license/review?reviewStatus=PENDING', '需认证', 'auth-required', null, true, 'businessLicense', '查询待审核/已审核的营业执照列表')}
  1336. ${testSection('admin-license-review', '审核执照', 'POST', 'post', '/api/admin/license/review', '需认证', 'auth-required',
  1337. [{label:'执照ID(licenseId)', name:'licenseId', placeholder:'1', default:'1'},
  1338. {label:'动作(action)', name:'action', placeholder:'APPROVED/REJECTED', default:'APPROVED'},
  1339. {label:'驳回原因(rejectReason)', name:'rejectReason', placeholder:'驳回时填写', default:''}], false, null, '审核营业执照,通过或驳回')}`;
  1340. }
  1341. function renderAdminPaymentModule() {
  1342. return `
  1343. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminPaymentTests()">运行全部支付方案测试</button></div>
  1344. ${testSection('admin-payment-create', '创建方案', 'POST', 'post', '/api/admin/payment/plans', '需认证', 'auth-required',
  1345. [{label:'方案编码(planCode)', name:'planCode', placeholder:'MONTHLY_PLUS', default:'MONTHLY_PLUS'},
  1346. {label:'方案名称(planName)', name:'planName', placeholder:'普通会员·月卡', default:'普通会员·月卡'},
  1347. {label:'价格(price)', name:'price', placeholder:'9.90', default:'9.90'},
  1348. {label:'有效天数(durationDays)', name:'durationDays', placeholder:'30', default:'30'},
  1349. {label:'会员等级(membershipLevel)', name:'membershipLevel', placeholder:'PLUS', default:'PLUS'}], false, null, '管理员创建新的支付方案')}
  1350. ${testSection('admin-payment-update', '更新方案', 'PUT', 'put', '/api/admin/payment/plans/{id}', '需认证', 'auth-required',
  1351. [{label:'方案ID(id)', name:'id', placeholder:'1', default:'1'},
  1352. {label:'方案编码(planCode)', name:'planCode', placeholder:'MONTHLY_PLUS', default:'MONTHLY_PLUS'},
  1353. {label:'方案名称(planName)', name:'planName', placeholder:'普通会员·月卡', default:'普通会员·月卡(更新)'},
  1354. {label:'价格(price)', name:'price', placeholder:'9.90', default:'9.90'},
  1355. {label:'有效天数(durationDays)', name:'durationDays', placeholder:'30', default:'30'},
  1356. {label:'会员等级(membershipLevel)', name:'membershipLevel', placeholder:'PLUS', default:'PLUS'}], false, null, '管理员更新支付方案')}
  1357. ${testSection('admin-payment-toggle', '切换方案状态', 'PUT', 'put', '/api/admin/payment/plans/{id}/status', '需认证', 'auth-required',
  1358. [{label:'方案ID(id)', name:'id', placeholder:'1', default:'1'},
  1359. {label:'状态(status)', name:'status', placeholder:'0下架/1上架', default:'0'}], false, null, '切换支付方案上架/下架状态')}`;
  1360. }
  1361. function renderAdminTrialQuotaModule() {
  1362. return `
  1363. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminTrialQuotaTests()">运行全部体验配额测试</button></div>
  1364. ${testSection('admin-tq-active', '当前体验配置', 'GET', 'get', '/api/trial-quota/active', '公开', 'auth-public', null, true, 'trialQuota', '获取当前激活的体验配额配置')}
  1365. ${testSection('admin-tq-list', '所有配置', 'GET', 'get', '/api/admin/trial-quota/configs', '需认证', 'auth-required', null, true, 'trialQuota', '查询所有体验配额配置列表')}
  1366. ${testSection('admin-tq-create', '创建配置', 'POST', 'post', '/api/admin/trial-quota/config', '需认证', 'auth-required',
  1367. [{label:'配置名称(configName)', name:'configName', placeholder:'新用户默认体验', default:'测试体验配置'},
  1368. {label:'体验天数(trialDays)', name:'trialDays', placeholder:'15', default:'15'},
  1369. {label:'日查询限制(dailyQueryLimit)', name:'dailyQueryLimit', placeholder:'20', default:'20'},
  1370. {label:'月关注限制(monthlyWatchlistLimit)', name:'monthlyWatchlistLimit', placeholder:'199', default:'199'},
  1371. {label:'配置类型(configType)', name:'configType', placeholder:'DEFAULT', default:'DEFAULT'},
  1372. {label:'用户类型(applicableUserType)', name:'applicableUserType', placeholder:'NEW_USER', default:'NEW_USER'}], false, 'trialQuota', '创建新的体验配额配置')}
  1373. ${testSection('admin-tq-update', '更新配置', 'PUT', 'put', '/api/admin/trial-quota/config/{id}', '需认证', 'auth-required',
  1374. [{label:'配置ID(id)', name:'id', placeholder:'1', default:'1'},
  1375. {label:'配置名称(configName)', name:'configName', placeholder:'更新后名称', default:'更新后的体验配置'},
  1376. {label:'体验天数(trialDays)', name:'trialDays', placeholder:'30', default:'30'},
  1377. {label:'日查询限制(dailyQueryLimit)', name:'dailyQueryLimit', placeholder:'50', default:'50'},
  1378. {label:'月关注限制(monthlyWatchlistLimit)', name:'monthlyWatchlistLimit', placeholder:'199', default:'199'}], false, 'trialQuota', '更新指定体验配额配置')}
  1379. ${testSection('admin-tq-delete', '删除配置', 'DELETE', 'delete', '/api/admin/trial-quota/config/{id}', '需认证', 'auth-required',
  1380. [{label:'配置ID(id)', name:'id', placeholder:'1', default:'1'}], false, null, '删除指定体验配额配置')}`;
  1381. }
  1382. function renderAdminUserModule() {
  1383. return `
  1384. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminUserTests()">运行全部用户管理测试</button></div>
  1385. ${testSection('admin-user-list', '用户列表', 'GET', 'get', '/api/admin/users?page={page}&size={size}&phone={phone}&role={role}', '需认证', 'auth-required',
  1386. [{label:'页码(page)', name:'page', placeholder:'1', default:'1'},
  1387. {label:'每页条数(size)', name:'size', placeholder:'10', default:'10'},
  1388. {label:'手机号(phone)', name:'phone', placeholder:'138', default:''},
  1389. {label:'角色(role)', name:'role', placeholder:'USER/ADMIN/SUPER_ADMIN', default:''}], true, null, '超级管理员查询用户列表')}
  1390. ${testSection('admin-user-role', '修改用户角色', 'PUT', 'put', '/api/admin/users/{userId}/role', '需认证', 'auth-required',
  1391. [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'},
  1392. {label:'目标角色(role)', name:'role', placeholder:'USER/ADMIN/SUPER_ADMIN', default:'ADMIN'}], false, null, '超级管理员修改用户角色')}`;
  1393. }
  1394. function renderAdminAuditModule() {
  1395. return `
  1396. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminAuditTests()">运行全部审计日志测试</button></div>
  1397. ${testSection('admin-audit-query', '查询审计日志', 'GET', 'get', '/api/admin/audit-log?page=1&size=10', '需认证', 'auth-required', null, true, 'auditLog', '分页查询操作审计日志')}
  1398. ${testSection('admin-audit-sensitive', '敏感操作日志', 'GET', 'get', '/api/admin/audit-log/sensitive', '需认证', 'auth-required', null, true, 'auditLog', '查询所有敏感操作日志')}
  1399. ${testSection('admin-audit-user', '用户操作日志', 'GET', 'get', '/api/admin/audit-log/user/{userId}?limit=20', '需认证', 'auth-required',
  1400. [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'}], true, 'auditLog', '查询指定用户的操作日志')}
  1401. ${testSection('admin-audit-target', '目标操作历史', 'GET', 'get', '/api/admin/audit-log/target/{targetType}/{targetId}?limit=20', '需认证', 'auth-required',
  1402. [{label:'目标类型(targetType)', name:'targetType', placeholder:'USER', default:'USER'},
  1403. {label:'目标ID(targetId)', name:'targetId', placeholder:'1', default:'1'}], true, 'auditLog', '查询某个目标对象的操作历史')}
  1404. ${testSection('admin-audit-stats', '模块操作统计', 'GET', 'get', '/api/admin/audit-log/stats?module=USER', '需认证', 'auth-required', null, true, null, '统计指定模块的操作次数')}`;
  1405. }
  1406. // ===================== 批量测试 =====================
  1407. function getSectionsInModule(modulePrefix) {
  1408. return Array.from(document.querySelectorAll('.test-section')).filter(s => {
  1409. const btn = s.querySelector('button[onclick*="runTest"]');
  1410. return btn && btn.getAttribute('onclick').includes("'" + modulePrefix + "-");
  1411. });
  1412. }
  1413. const USER_MODULES = ['auth','crawler','invite','coupon','membership','payment','upload','search','platform-account','business-license','platform-config'];
  1414. const ADMIN_MODULES = ['admin-membership','admin-invite','admin-crawler','admin-coupon','admin-license','admin-payment','admin-trial-quota','admin-user','admin-audit','upload'];
  1415. function countTestsInCurrentPanel() {
  1416. return document.querySelectorAll('.test-section button[onclick*="runTest"]').length;
  1417. }
  1418. async function runBatchTests(modulePrefix) {
  1419. const sections = getSectionsInModule(modulePrefix);
  1420. // 将该模块中尚未运行过的测试计入total
  1421. for (const section of sections) {
  1422. const btn = section.querySelector('button[onclick*="runTest"]');
  1423. if (btn) {
  1424. const onclick = btn.getAttribute('onclick') || '';
  1425. const match = onclick.match(/runTest\('([^']+)'/);
  1426. if (match && !stats.hasOwnProperty(match[1])) {
  1427. stats.total++;
  1428. }
  1429. }
  1430. }
  1431. stats.pending = Math.max(0, stats.total - stats.pass - stats.fail);
  1432. document.getElementById('totalCount').textContent = stats.total;
  1433. document.getElementById('pendingCount').textContent = stats.pending;
  1434. for (const section of sections) {
  1435. const btn = section.querySelector('button[onclick*="runTest"]');
  1436. if (btn) {
  1437. btn.click();
  1438. await new Promise(r => setTimeout(r, 600));
  1439. }
  1440. }
  1441. }
  1442. async function runAllTests() {
  1443. resetStats();
  1444. // 先遍历用户侧模块
  1445. for (const m of USER_MODULES) {
  1446. const tab = document.querySelector(`#moduleTabs .module-tab[data-module="${m}"]`);
  1447. if (tab) tab.click();
  1448. await new Promise(r => setTimeout(r, 50));
  1449. stats.total += countTestsInCurrentPanel();
  1450. }
  1451. // 再遍历运营侧模块
  1452. for (const m of ADMIN_MODULES) {
  1453. const tab = document.querySelector(`#adminModuleTabs .module-tab[data-module="${m}"]`);
  1454. if (tab) tab.click();
  1455. await new Promise(r => setTimeout(r, 50));
  1456. stats.total += countTestsInCurrentPanel();
  1457. }
  1458. document.getElementById('totalCount').textContent = stats.total;
  1459. stats.pending = stats.total;
  1460. document.getElementById('pendingCount').textContent = stats.pending;
  1461. // 逐模块执行 - 用户侧
  1462. switchSide('user');
  1463. for (const m of USER_MODULES) {
  1464. try {
  1465. const tab = document.querySelector(`#moduleTabs .module-tab[data-module="${m}"]`);
  1466. if (tab) tab.click();
  1467. await new Promise(r => setTimeout(r, 100));
  1468. await runBatchTests(m);
  1469. } catch (e) { console.error('模块 ' + m + ' 测试出错:', e); }
  1470. }
  1471. // 逐模块执行 - 运营侧
  1472. switchSide('admin');
  1473. for (const m of ADMIN_MODULES) {
  1474. try {
  1475. const tab = document.querySelector(`#adminModuleTabs .module-tab[data-module="${m}"]`);
  1476. if (tab) tab.click();
  1477. await new Promise(r => setTimeout(r, 100));
  1478. await runBatchTests(m);
  1479. } catch (e) { console.error('模块 ' + m + ' 测试出错:', e); }
  1480. }
  1481. }
  1482. function resetStats() {
  1483. const keys = Object.keys(stats);
  1484. for (const k of keys) {
  1485. if (k !== 'pass' && k !== 'fail' && k !== 'pending' && k !== 'total') {
  1486. delete stats[k];
  1487. }
  1488. }
  1489. stats.pass = 0;
  1490. stats.fail = 0;
  1491. stats.pending = 0;
  1492. stats.total = 0;
  1493. document.getElementById('totalCount').textContent = '0';
  1494. document.getElementById('passCount').textContent = '0';
  1495. document.getElementById('failCount').textContent = '0';
  1496. document.getElementById('pendingCount').textContent = '0';
  1497. }
  1498. function clearAllResults() {
  1499. stopAlipayPolling();
  1500. closeAlipayModal();
  1501. document.querySelectorAll('.response-box').forEach(b => b.style.display = 'none');
  1502. document.querySelectorAll('.status-badge').forEach(b => { b.textContent = ''; b.className = 'status-badge'; });
  1503. resetStats();
  1504. }
  1505. // ===================== 支付宝支付弹窗 =====================
  1506. let currentAlipayModal = null;
  1507. let currentAlipayOrderNo = null;
  1508. function showAlipayModal(qrBase64, amount, orderNo) {
  1509. closeAlipayModal();
  1510. currentAlipayOrderNo = orderNo;
  1511. const overlay = document.createElement('div');
  1512. overlay.className = 'alipay-modal-overlay';
  1513. overlay.id = 'alipay-modal-overlay';
  1514. overlay.onclick = (e) => { if (e.target === overlay) closeAlipayModal(); };
  1515. const amountText = amount ? '<span>¥</span>' + amount : '';
  1516. overlay.innerHTML = '<div class="alipay-modal">'
  1517. + '<button class="alipay-modal-close" onclick="closeAlipayModal()" title="关闭">×</button>'
  1518. + '<div class="alipay-modal-logo">'
  1519. + '<svg viewBox="0 0 1024 1024"><path fill="#1677ff" d="M876.8 128H147.2C114.9 128 89 154.4 89 186.7v650.6c0 32.3 25.9 58.7 58.2 58.7h729.6c32.3 0 58.2-26.4 58.2-58.7V186.7c.6-32.3-25.3-58.7-57.2-58.7zM512 714.7c-112 0-202.7-91.2-202.7-203.5 0-44.8 14.4-86.4 39.5-120.3C320 416 288 416 256 416V352c53.3 0 101.3-10.7 144-32 35.2-17.6 66.7-41.6 92.8-70.4 27.2-30.4 48-65.6 61.9-104h140.8v64h-64c8.5 30.9 13.3 63.5 13.3 97.1 0 48-9.6 92.8-27.7 133.3C690.7 464 729.6 480 768 480v64c-53.3 0-101.3-10.7-144-32-19.2 34.1-47.5 62.9-81.6 83.2 74.7 21.3 153.6 26.7 234.7 16v64c-93.9 10.7-185.6 3.2-270.9-26.7C464 693.3 432 704 400 704v-64c32 0 64-2.1 94.9-6.4C458.7 604.8 437.3 558.9 437.3 509.3c0-93.9 76.8-170.1 171.5-170.1 10.7 0 21.3 1.1 31.5 3.2-4.8-25.1-7.5-51.2-7.5-78.4 0-9.6.5-19.2 1.6-28.3-43.2 21.3-92.8 34.1-145.6 36.8V192c85.3 6.4 162.1 41.6 220.3 96 29.9-5.9 61.3-9.1 93.3-9.1V341.3c-26.7 0-52.8 2.7-77.9 7.5C755.2 384 768 416 768 448v32h-64v-32c0-53.3-43.7-96.5-97.1-96.5-42.7 0-78.9 27.7-92.3 65.6 36.3 15.5 61.9 51.7 61.9 94.1 0 56.5-46.1 102.4-102.9 102.4S370.7 565.3 370.7 509.3c0-71.5 61.3-129.6 136.5-129.6 4.3 0 8.5.5 12.8 1.1-8-29.9-12.3-61.3-12.3-93.9 0-4.3.5-8.5.5-12.8-52.3 4.8-100.3 22.4-140.3 49.6-34.1 23.5-62.4 53.3-83.2 88-18.7 31.5-29.3 68.3-29.3 107.2 0 112 90.7 203.5 202.7 203.5 62.9 0 118.9-28.8 156.3-73.6l48 42.7C660.5 677.3 590.9 714.7 512 714.7z"/></svg>'
  1520. + '支付宝'
  1521. + '</div>'
  1522. + '<div class="alipay-modal-title">扫一扫付款</div>'
  1523. + '<div class="alipay-modal-amount">' + amountText + '</div>'
  1524. + '<div class="alipay-modal-qr"><img src="' + qrBase64 + '" alt="支付宝付款码"></div>'
  1525. + '<div class="alipay-modal-status" id="alipay-modal-status">轮询支付状态中...</div>'
  1526. + '<div class="alipay-modal-hint">二维码有效期 15 分钟<br>请尽快完成付款</div>'
  1527. + '</div>';
  1528. document.body.appendChild(overlay);
  1529. currentAlipayModal = overlay;
  1530. if (orderNo) startAlipayPolling(orderNo);
  1531. }
  1532. function closeAlipayModal() {
  1533. if (currentAlipayModal) {
  1534. currentAlipayModal.remove();
  1535. currentAlipayModal = null;
  1536. currentAlipayOrderNo = null;
  1537. }
  1538. }
  1539. // ===================== 支付宝支付轮询 =====================
  1540. let alipayPollTimer = null;
  1541. function startAlipayPolling(orderNo) {
  1542. stopAlipayPolling();
  1543. const statusEl = document.getElementById('alipay-modal-status');
  1544. if (statusEl) statusEl.textContent = '轮询支付状态中...';
  1545. alipayPollTimer = setInterval(async () => {
  1546. try {
  1547. const res = await api('GET', '/api/payment/order/' + orderNo, null, true);
  1548. const status = res.data?.data?.status;
  1549. const el = document.getElementById('alipay-modal-status');
  1550. if (!el || !currentAlipayModal) { stopAlipayPolling(); return; }
  1551. if (status === 'PAID') {
  1552. stopAlipayPolling();
  1553. el.innerHTML = '<span style="color:#2e7d32;font-weight:600;">✓ 支付成功!订单已激活</span>';
  1554. } else if (status === 'EXPIRED') {
  1555. stopAlipayPolling();
  1556. el.innerHTML = '<span style="color:#c62828;">订单已过期</span>';
  1557. } else {
  1558. el.textContent = '轮询支付状态中... (当前: ' + (status || '未知') + ')';
  1559. }
  1560. } catch (e) {
  1561. console.error('轮询支付状态异常:', e);
  1562. }
  1563. }, 3000);
  1564. }
  1565. function stopAlipayPolling() {
  1566. if (alipayPollTimer) { clearInterval(alipayPollTimer); alipayPollTimer = null; }
  1567. }
  1568. // 用户侧快捷批量运行
  1569. function runAuthTests() { runBatchTests('auth'); }
  1570. function runCrawlerTests() { runBatchTests('crawler'); }
  1571. function runInviteTests() { runBatchTests('invite'); }
  1572. function runCouponTests() { runBatchTests('coupon'); }
  1573. function runMembershipTests() { runBatchTests('membership'); }
  1574. function runPaymentTests() { runBatchTests('payment'); }
  1575. function runUploadTests() { uploadFile(); }
  1576. function runSearchTests() { runBatchTests('search'); }
  1577. function runPlatformAccountTests() { runBatchTests('pa'); }
  1578. function runBusinessLicenseTests() { runBatchTests('bl'); }
  1579. function runPlatformConfigTests() { runBatchTests('pc'); }
  1580. function runSearchRecordTests() { runBatchTests('sr'); }
  1581. function runPlatformSessionTests() { runBatchTests('ps'); }
  1582. // 运营侧快捷批量运行
  1583. function runAdminMembershipTests() { runBatchTests('admin-membership'); }
  1584. function runAdminInviteTests() { runBatchTests('admin-invite'); }
  1585. function runAdminCrawlerTests() { runBatchTests('admin-crawler'); }
  1586. function runAdminCouponTests() { runBatchTests('admin-coupon'); }
  1587. function runAdminLicenseTests() { runBatchTests('admin-license'); }
  1588. function runAdminPaymentTests() { runBatchTests('admin-payment'); }
  1589. function runAdminTrialQuotaTests() { runBatchTests('admin-tq'); }
  1590. function runAdminUserTests() { runBatchTests('admin-user'); }
  1591. function runAdminAuditTests() { runBatchTests('admin-audit'); }
  1592. // ===================== 验证器 =====================
  1593. function validate_authSend(result) { return result.data?.code === 200; }
  1594. function validate_authSmslogin(result) { return result.data?.code === 200 && result.data?.data?.accessToken; }
  1595. function validate_authPwdlogin(result) { return result.data?.code === 200 && result.data?.data?.accessToken; }
  1596. function validate_authUserinfo(result) { return result.data?.code === 200 && result.data?.data?.userId; }
  1597. function validate_crawlerStatus(result) { return result.data?.code === 200 && result.data?.data?.todayQuota !== undefined; }
  1598. function validate_crawlerConsume(result) { return result.data?.code === 200; }
  1599. function validate_crawlerLogs(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1600. function validate_inviteCode(result) { return result.data?.code === 200 && result.data?.data?.code; }
  1601. function validate_inviteStats(result) { return result.data?.code === 200 && result.data?.data?.totalInvited !== undefined; }
  1602. function validate_inviteRewards(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1603. function validate_invitePage(result) { return result.data?.code === 200 && result.data?.data?.inviteCode; }
  1604. function validate_inviteLinkEntry(result) { return result.data?.code === 200 && result.data?.data?.inviteCode; }
  1605. function validate_couponMy(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1606. function validate_couponAvailable(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1607. function validate_membershipMy(result) { return result.data?.code === 200; }
  1608. function validate_membershipBenefits(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1609. function validate_membershipAdminGrant(result) { return result.data?.code === 200 && result.data?.data?.level; }
  1610. function validate_membershipAdminList(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1611. function validate_paList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1612. function validate_blInfo(result) { return result.data?.code === 200; }
  1613. function validate_blReviewList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1614. function validate_pcEnabled(result) { return result.data?.code === 200 && Array.isArray(result.data?.data) && result.data?.data?.length >= 3; }
  1615. function validate_pcDetail(result) { return result.data?.code === 200 && result.data?.data?.platformCode; }
  1616. function validate_tqActive(result) { return result.data?.code === 200 && result.data?.data; }
  1617. function validate_tqList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1618. function validate_tqCreate(result) { return result.data?.code === 200 && result.data?.data; }
  1619. function validate_tqUpdate(result) { return result.data?.code === 200 && result.data?.data; }
  1620. function validate_adminInviteConfig(result) { return result.data?.code === 200; }
  1621. function validate_adminInviteConfigs(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1622. function validate_adminGrants(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1623. function validate_adminInviteRewards(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1624. function validate_adminUserDetail(result) { return result.data?.code === 200; }
  1625. function validate_adminConversionStats(result) { return result.data?.code === 200 && result.data?.data; }
  1626. function validate_adminUserList(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1627. function validate_adminUserRole(result) { return result.data?.code === 200; }
  1628. function validate_auditQuery(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1629. function validate_auditSensitive(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1630. function validate_auditUser(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1631. function validate_auditTarget(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1632. function validate_auditStats(result) { return result.data?.code === 200 && result.data?.data?.count !== undefined; }
  1633. // 运营侧验证器
  1634. function validate_adminMembershipGrant(result) { return result.data?.code === 200 && result.data?.data?.level; }
  1635. function validate_adminMembershipList(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1636. function validate_adminInviteConfig(result) { return result.data?.code === 200; }
  1637. function validate_adminInviteConfigs(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1638. function validate_adminInviteRewards(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1639. function validate_adminInviteConversion(result) { return result.data?.code === 200 && result.data?.data; }
  1640. function validate_adminCrawlerGrant(result) { return result.data?.code === 200 && result.data?.data; }
  1641. function validate_adminCrawlerGrants(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1642. function validate_adminCrawlerUserDetail(result) { return result.data?.code === 200; }
  1643. function validate_adminCouponIssue(result) { return result.data?.code === 200; }
  1644. function validate_adminLicenseList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1645. function validate_adminPaymentCreate(result) { return result.data?.code === 200; }
  1646. function validate_adminPaymentUpdate(result) { return result.data?.code === 200; }
  1647. function validate_adminPaymentToggle(result) { return result.data?.code === 200; }
  1648. function validate_adminTqActive(result) { return result.data?.code === 200 && result.data?.data; }
  1649. function validate_adminTqList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1650. function validate_adminTqCreate(result) { return result.data?.code === 200 && result.data?.data; }
  1651. function validate_adminTqUpdate(result) { return result.data?.code === 200 && result.data?.data; }
  1652. function validate_adminUserList(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1653. function validate_adminUserRole(result) { return result.data?.code === 200; }
  1654. function validate_adminAuditQuery(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1655. function validate_adminAuditSensitive(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1656. function validate_adminAuditUser(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1657. function validate_adminAuditTarget(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1658. function validate_adminAuditStats(result) { return result.data?.code === 200; }
  1659. // 支付模块验证器
  1660. function validate_paymentPlans(result) { return result.data?.code === 200 && Array.isArray(result.data?.data) && result.data?.data?.length >= 2; }
  1661. function validate_paymentOrders(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1662. function validate_searchQuery(result) { return result.data?.code === 200 && result.data?.data?.prices !== undefined; }
  1663. function validate_searchSuggestions(result) { return result.data?.code === 200 && Array.isArray(result.data?.data) && result.data?.data?.length > 0; }
  1664. function validate_searchHistory(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1665. function validate_searchSave(result) { return result.data?.code === 200 && result.data?.data?.keyword; }
  1666. function validate_searchRecords(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1667. // 搜索记录模块验证器
  1668. function validate_srSave(result) { return result.data?.code === 200 && result.data?.data?.id && result.data?.data?.drugName; }
  1669. function validate_srList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1670. function validate_srDetail(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1671. // 平台会话模块验证器
  1672. function validate_psUpload(result) { return result.data?.code === 200 && result.data?.data?.platformCode; }
  1673. function validate_psList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1674. // ===================== 初始化 =====================
  1675. window.addEventListener('DOMContentLoaded', () => {
  1676. const params = new URLSearchParams(window.location.search);
  1677. if (params.get('baseUrl')) document.getElementById('baseUrl').value = params.get('baseUrl');
  1678. // 渲染默认模块
  1679. renderModule('auth');
  1680. });
  1681. </script>
  1682. </body>
  1683. </html>