invite-test.html 137 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023
  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">管理员登录(t_admin表)</span>
  189. <label>用户名</label>
  190. <label for="adminPwdUsername"></label><input type="text" id="adminPwdUsername" value="admin" placeholder="管理员用户名" style="width:150px;">
  191. <label>密码</label>
  192. <label for="adminPwdPassword"></label><input type="password" id="adminPwdPassword" value="admin@123" placeholder="管理员密码" style="width:140px;">
  193. <button class="btn btn-warning btn-sm" onclick="adminPasswordLogin()">管理员登录</button>
  194. </div>
  195. </div>
  196. </div>
  197. <!-- 测试统计栏 -->
  198. <div class="summary-bar">
  199. <span style="font-weight:600;">本页测试统计:</span>
  200. <div class="summary-item"><span style="font-weight:700;font-size:20px;color:#1565c0;" id="totalCount">0</span><span>总计</span></div>
  201. <div class="summary-item"><span class="summary-count pass" id="passCount">0</span><span>通过</span></div>
  202. <div class="summary-item"><span class="summary-count fail" id="failCount">0</span><span>失败</span></div>
  203. <div class="summary-item"><span class="summary-count pending" id="pendingCount">0</span><span>待测</span></div>
  204. <button class="btn btn-warning btn-sm" onclick="runAllTests()" style="margin-left:auto;">一键全部测试</button>
  205. <button class="btn btn-outline btn-sm" onclick="clearAllResults()">清除结果</button>
  206. </div>
  207. <!-- 用户侧模块标签栏 -->
  208. <div class="module-tabs" id="moduleTabs">
  209. <div class="module-tab active" data-module="auth">1.认证模块</div>
  210. <div class="module-tab" data-module="crawler">2.爬虫配额</div>
  211. <div class="module-tab" data-module="invite">3.邀请系统</div>
  212. <div class="module-tab" data-module="coupon">4.优惠券</div>
  213. <div class="module-tab" data-module="membership">5.会员等级</div>
  214. <div class="module-tab" data-module="payment">6.支付模块</div>
  215. <div class="module-tab" data-module="upload">7.文件上传</div>
  216. <div class="module-tab" data-module="search">8.搜索查询</div>
  217. <div class="module-tab" data-module="platform-account">9.平台账号</div>
  218. <div class="module-tab" data-module="business-license">10.营业执照</div>
  219. <div class="module-tab" data-module="platform-config">11.平台配置</div>
  220. <div class="module-tab" data-module="search-record">12.搜索记录</div>
  221. <div class="module-tab" data-module="platform-session">13.平台会话</div>
  222. </div>
  223. <!-- 运营侧模块标签栏 -->
  224. <div class="module-tabs admin-tabs" id="adminModuleTabs" style="display:none;">
  225. <div class="module-tab active" data-module="admin-membership">1.会员管理</div>
  226. <div class="module-tab" data-module="admin-invite">2.邀请管理</div>
  227. <div class="module-tab" data-module="admin-crawler">3.爬虫管理</div>
  228. <div class="module-tab" data-module="admin-coupon">4.优惠券发放</div>
  229. <div class="module-tab" data-module="admin-license">5.执照审核</div>
  230. <div class="module-tab" data-module="admin-payment">6.支付方案</div>
  231. <div class="module-tab" data-module="admin-anxin">7.安心付管理</div>
  232. <div class="module-tab" data-module="admin-trial-quota">8.体验配额</div>
  233. <div class="module-tab" data-module="admin-user">9.用户管理</div>
  234. <div class="module-tab" data-module="admin-audit">10.审计日志</div>
  235. <div class="module-tab" data-module="upload">11.文件上传</div>
  236. </div>
  237. <div id="panels"></div>
  238. </div>
  239. <script>
  240. // ===================== 全局状态 =====================
  241. let activeSide = 'user';
  242. const BASE_URL = () => document.getElementById('baseUrl').value;
  243. const TOKEN = () => activeSide === 'admin'
  244. ? (document.getElementById('adminToken')?.value || '')
  245. : (document.getElementById('token')?.value || '');
  246. const PHONE = () => activeSide === 'admin'
  247. ? (document.getElementById('adminPhone')?.value || '')
  248. : (document.getElementById('phone')?.value || '');
  249. const SMS_CODE = () => activeSide === 'admin'
  250. ? (document.getElementById('adminSmsCode')?.value || '123456')
  251. : (document.getElementById('smsCode')?.value || '123456');
  252. let stats = { pass: 0, fail: 0, pending: 0, total: 0 };
  253. // ===================== 大整数安全JSON解析 =====================
  254. /**
  255. * 安全解析JSON:将16位以上的大整数(如雪花ID)包裹为字符串后再解析,
  256. * 避免JavaScript Number精度丢失(如2069620495151230978 → 2069620495151231000)
  257. * 示例:":2069620495151230978," → ":\"2069620495151230978\","
  258. */
  259. function safeJsonParse(text) {
  260. if (!text || text.trim() === '' || text.trim() === 'null') return null;
  261. const safeText = text.replace(/([:,\[]\s*)(\d{16,})(\s*[,\]\}])/g, '$1"$2"$3');
  262. return JSON.parse(safeText);
  263. }
  264. // ===================== HTTP 请求 =====================
  265. async function api(method, path, body, requireAuth = true) {
  266. const headers = { 'Content-Type': 'application/json' };
  267. if (requireAuth && TOKEN()) { headers['Authorization'] = 'Bearer ' + TOKEN(); }
  268. const opts = { method, headers };
  269. if (body && method !== 'GET') { opts.body = JSON.stringify(body); }
  270. const startTime = Date.now();
  271. try {
  272. const res = await fetch(BASE_URL() + path, opts);
  273. const text = await res.text();
  274. let data;
  275. try { data = safeJsonParse(text); } catch (e) { data = { _raw: text }; }
  276. return { ok: res.ok, status: res.status, data, duration: Date.now() - startTime };
  277. } catch (e) {
  278. return { ok: false, status: 0, data: { error: e.message }, duration: Date.now() - startTime };
  279. }
  280. }
  281. // ===================== 测试执行 =====================
  282. async function runTest(sectionId, method, path, body, requireAuth, validatorName) {
  283. const box = document.getElementById(sectionId + '-box');
  284. const statusEl = document.getElementById(sectionId + '-status');
  285. const rawEl = document.getElementById(sectionId + '-raw');
  286. const durEl = document.getElementById(sectionId + '-duration');
  287. updateBadge(statusEl, 'pending', '运行中...');
  288. if (box) box.style.display = 'block';
  289. let resolvedPath = path;
  290. document.querySelectorAll(`[id^="${sectionId}-"]`).forEach(input => {
  291. if (input.tagName === 'INPUT') {
  292. const fieldName = input.id.replace(sectionId + '-', '');
  293. // 替换路径变量 {fieldName}
  294. resolvedPath = resolvedPath.replace('{' + fieldName + '}', input.value || fieldName);
  295. // 替换查询参数 ?fieldName=xxx 或 &fieldName=xxx(输入框有值时才替换)
  296. if (input.value) {
  297. const queryVal = encodeURIComponent(input.value);
  298. resolvedPath = resolvedPath.replace(
  299. new RegExp('([?&])' + fieldName + '=[^&]*', 'g'),
  300. '$1' + fieldName + '=' + queryVal
  301. );
  302. }
  303. }
  304. });
  305. const result = await api(method, resolvedPath, body, requireAuth);
  306. if (durEl) durEl.textContent = result.duration + 'ms';
  307. if (rawEl) rawEl.textContent = JSON.stringify(result.data, null, 2);
  308. // 检测响应中的支付结果并渲染
  309. const qrBase64 = result.data?.data?.qrCodeBase64;
  310. const payUrl = result.data?.data?.payUrl;
  311. const channel = result.data?.data?.channel;
  312. const orderNo = result.data?.data?.orderNo;
  313. const amount = result.data?.data?.amount;
  314. // 自动回填订单号到相关输入框
  315. if (orderNo) {
  316. ['payment-query-order-orderNo', 'payment-mock-pay-orderNo',
  317. 'payment-callback-wechat-out_trade_no', 'payment-callback-alipay-out_trade_no']
  318. .forEach(fid => { const el = document.getElementById(fid); if (el) el.value = orderNo; });
  319. }
  320. // 自动回填协议ID到相关输入框
  321. const agreementId = result.data?.data?.agreementId;
  322. if (agreementId) {
  323. ['payment-agreement-detail-agreementId', 'payment-agreement-unsign-agreementId']
  324. .forEach(fid => { const el = document.getElementById(fid); if (el) el.value = agreementId; });
  325. }
  326. // 自动回填签约协议号到微信回调通知
  327. const externalAgreementNo = result.data?.data?.externalAgreementNo;
  328. if (externalAgreementNo) {
  329. const el = document.getElementById('payment-agreement-notify-wechat-out_contract_code');
  330. if (el) el.value = externalAgreementNo;
  331. }
  332. // 自动回填安心付卡模板ID到相关输入框
  333. const anxinCardId = result.data?.data?.cardId;
  334. if (anxinCardId) {
  335. ['admin-anxin-sync-cardId', 'admin-anxin-status-cardId',
  336. 'payment-anxin-notify-deduct-card_id', 'payment-anxin-notify-card-change-card_id']
  337. .forEach(fid => { const el = document.getElementById(fid); if (el) el.value = anxinCardId; });
  338. }
  339. if (channel === 'ALIPAY' && qrBase64) {
  340. // 支付宝:DeepSeek 风格自定义弹窗,展示二维码
  341. showAlipayModal(qrBase64, amount, orderNo);
  342. } else if (qrBase64) {
  343. // 微信:显示扫码二维码
  344. let qrBox = document.getElementById(sectionId + '-qrcode');
  345. if (!qrBox) {
  346. qrBox = document.createElement('div');
  347. qrBox.id = sectionId + '-qrcode';
  348. qrBox.style.cssText = 'text-align:center;padding:12px;background:#fff;border-radius:4px;margin-top:8px;';
  349. rawEl.parentNode.insertBefore(qrBox, rawEl.nextSibling);
  350. }
  351. const channelName = channel === 'WECHAT' ? '微信' : (channel || '相应平台');
  352. 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;">' + channelName + '扫码付款后回调自动激活,也可手动确认支付测试回调</div>';
  353. } else {
  354. const oldBox = document.getElementById(sectionId + '-qrcode');
  355. if (oldBox) oldBox.remove();
  356. }
  357. let passed = false;
  358. if (validatorName && typeof window[validatorName] === 'function') {
  359. try { passed = window[validatorName](result); } catch (e) { passed = false; }
  360. } else {
  361. passed = result.ok && result.status === 200;
  362. }
  363. updateStats(sectionId, passed);
  364. updateBadge(statusEl, passed ? 'success' : 'error', passed ? '通过 ' + result.status : '失败 ' + result.status);
  365. if (box) box.style.display = 'block';
  366. }
  367. function updateBadge(el, cls, text) {
  368. if (!el) return;
  369. el.textContent = text;
  370. el.className = 'status-badge status-' + cls;
  371. }
  372. function updateStats(id, passed) {
  373. const old = stats[id];
  374. if (old === 'pass') stats.pass--;
  375. if (old === 'fail') stats.fail--;
  376. stats[id] = passed ? 'pass' : 'fail';
  377. if (passed) stats.pass++; else stats.fail++;
  378. stats.pending = Math.max(0, stats.total - stats.pass - stats.fail);
  379. document.getElementById('passCount').textContent = stats.pass;
  380. document.getElementById('failCount').textContent = stats.fail;
  381. document.getElementById('pendingCount').textContent = stats.pending;
  382. }
  383. // ===================== 登录相关 =====================
  384. async function sendSms() {
  385. const res = await api('POST', '/api/auth/sms/send', { phone: PHONE(), scene: 'login' }, false);
  386. if (res.ok) {
  387. if (activeSide === 'admin') {
  388. const el = document.getElementById('adminSmsCode');
  389. if (el) el.value = '123456';
  390. } else {
  391. document.getElementById('smsCode').value = '123456';
  392. }
  393. const label = activeSide === 'admin' ? '管理员验证码' : '验证码';
  394. alert(label + '已写入(开发测试模式,固定验证码: 123456)');
  395. } else {
  396. alert('发送失败: ' + JSON.stringify(res.data));
  397. }
  398. }
  399. async function smsLogin() {
  400. const phone = PHONE();
  401. const code = SMS_CODE();
  402. if (!phone) { alert(activeSide === 'admin' ? '请填写管理员手机号' : '请填写手机号'); return; }
  403. const payload = { phone, code, scene: 'login' };
  404. const res = await api('POST', '/api/auth/sms/login', payload, false);
  405. if (res.data?.code === 200 && res.data?.data?.accessToken) {
  406. if (activeSide === 'admin') {
  407. document.getElementById('adminToken').value = res.data.data.accessToken;
  408. updateAdminRoleBadge(res.data.data.role);
  409. alert('管理员短信登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'USER'));
  410. } else {
  411. document.getElementById('token').value = res.data.data.accessToken;
  412. updateRoleBadge(res.data.data.role);
  413. alert('短信登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'USER'));
  414. }
  415. } else {
  416. alert('登录失败: ' + (res.data?.message || '未知错误'));
  417. }
  418. }
  419. async function passwordLogin() {
  420. const password = activeSide === 'admin'
  421. ? document.getElementById('adminPwdPassword').value
  422. : document.getElementById('pwdPassword').value;
  423. let payload;
  424. if (activeSide === 'admin') {
  425. const username = document.getElementById('adminPwdUsername').value;
  426. if (!username || !password) { alert('请填写用户名和密码'); return; }
  427. payload = { username, password };
  428. } else {
  429. const account = document.getElementById('pwdAccount').value.trim();
  430. if (!account || !password) { alert('请填写手机号/用户名和密码'); return; }
  431. // 自动识别:纯数字视为手机号,否则视为用户名
  432. if (/^\d{11}$/.test(account)) {
  433. payload = { phone: account, password };
  434. } else {
  435. payload = { username: account, password };
  436. }
  437. }
  438. const res = await api('POST', '/api/auth/password/login', payload, false);
  439. if (res.data?.code === 200 && res.data?.data?.accessToken) {
  440. if (activeSide === 'admin') {
  441. document.getElementById('adminToken').value = res.data.data.accessToken;
  442. updateAdminRoleBadge(res.data.data.role);
  443. alert('管理员密码登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'USER'));
  444. } else {
  445. document.getElementById('token').value = res.data.data.accessToken;
  446. updateRoleBadge(res.data.data.role);
  447. alert('密码登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'USER'));
  448. }
  449. } else {
  450. alert('登录失败: ' + (res.data?.message || '未知错误'));
  451. }
  452. }
  453. function updateRoleBadge(role) {
  454. const badge = document.getElementById('roleBadge');
  455. if (badge) {
  456. if (role) {
  457. badge.textContent = role;
  458. badge.style.display = 'inline-block';
  459. } else {
  460. badge.style.display = 'none';
  461. }
  462. }
  463. }
  464. function updateAdminRoleBadge(role) {
  465. const badge = document.getElementById('adminRoleBadge');
  466. if (badge) {
  467. if (role) {
  468. badge.textContent = role;
  469. badge.style.display = 'inline-block';
  470. } else {
  471. badge.style.display = 'none';
  472. }
  473. }
  474. }
  475. async function adminPasswordLogin() {
  476. const username = document.getElementById('adminPwdUsername').value;
  477. const password = document.getElementById('adminPwdPassword').value;
  478. if (!username || !password) { alert('请填写用户名和密码'); return; }
  479. const payload = { username, password };
  480. const res = await api('POST', '/api/auth/password/login', payload, false);
  481. if (res.data?.code === 200 && res.data?.data?.accessToken) {
  482. document.getElementById('adminToken').value = res.data.data.accessToken;
  483. updateAdminRoleBadge(res.data.data.role);
  484. alert('管理员登录成功!\n用户ID: ' + res.data.data.userId + '\n角色: ' + (res.data.data.role || 'ADMIN'));
  485. } else {
  486. alert('登录失败: ' + (res.data?.message || '未知错误'));
  487. }
  488. }
  489. function copyToken() {
  490. const tok = TOKEN();
  491. if (!tok) { alert('请先登录'); return; }
  492. navigator.clipboard.writeText(tok).then(() => alert('Token已复制!'));
  493. }
  494. async function sendCodeToField(inputId, scene) {
  495. const phoneInput = document.getElementById(inputId);
  496. if (!phoneInput || !phoneInput.value) { alert('请先填写手机号'); return; }
  497. const res = await api('POST', '/api/auth/sms/send', { phone: phoneInput.value, scene: scene || 'login' }, false);
  498. if (res.ok) {
  499. alert('验证码已发送(开发测试模式,固定验证码: 123456)');
  500. } else {
  501. alert('发送失败: ' + JSON.stringify(res.data));
  502. }
  503. }
  504. async function uploadFile() {
  505. const fileInput = document.getElementById('upload-file');
  506. const statusEl = document.getElementById('upload-status');
  507. const rawEl = document.getElementById('upload-raw');
  508. const durEl = document.getElementById('upload-duration');
  509. const box = document.getElementById('upload-result-box');
  510. if (!fileInput || !fileInput.files || !fileInput.files[0]) {
  511. alert('请先选择文件'); return;
  512. }
  513. const formData = new FormData();
  514. formData.append('file', fileInput.files[0]);
  515. updateBadge(statusEl, 'pending', '上传中...');
  516. box.style.display = 'block';
  517. const startTime = Date.now();
  518. try {
  519. const headers = {};
  520. if (TOKEN()) { headers['Authorization'] = 'Bearer ' + TOKEN(); }
  521. const res = await fetch(BASE_URL() + '/api/upload', { method: 'POST', headers, body: formData });
  522. const text = await res.text();
  523. const duration = Date.now() - startTime;
  524. let data;
  525. try { data = safeJsonParse(text); } catch (e) { data = { _raw: text }; }
  526. if (durEl) durEl.textContent = duration + 'ms';
  527. if (rawEl) rawEl.textContent = JSON.stringify(data, null, 2);
  528. const passed = res.ok && data?.code === 200;
  529. updateBadge(statusEl, passed ? 'success' : 'error', passed ? '通过 ' + res.status : '失败 ' + res.status);
  530. if (!stats.hasOwnProperty('upload-file')) {
  531. stats.total++;
  532. document.getElementById('totalCount').textContent = stats.total;
  533. }
  534. updateStats('upload-file', passed);
  535. } catch (e) {
  536. if (rawEl) rawEl.textContent = 'Error: ' + e.message;
  537. updateBadge(statusEl, 'error', '失败');
  538. }
  539. }
  540. // ===================== 侧切换 =====================
  541. function switchSide(side) {
  542. activeSide = side;
  543. document.querySelectorAll('.side-btn').forEach(b => b.classList.remove('active'));
  544. document.querySelector('.side-btn.' + side + '-side').classList.add('active');
  545. const isAdmin = side === 'admin';
  546. document.getElementById('userLoginSection').style.display = isAdmin ? 'none' : '';
  547. document.getElementById('adminLoginSection').style.display = isAdmin ? '' : 'none';
  548. document.getElementById('moduleTabs').style.display = isAdmin ? 'none' : '';
  549. document.getElementById('adminModuleTabs').style.display = isAdmin ? '' : 'none';
  550. if (isAdmin) {
  551. const activeTab = document.querySelector('#adminModuleTabs .module-tab.active');
  552. renderModule(activeTab ? activeTab.dataset.module : 'admin-membership');
  553. } else {
  554. const activeTab = document.querySelector('#moduleTabs .module-tab.active');
  555. renderModule(activeTab ? activeTab.dataset.module : 'auth');
  556. }
  557. }
  558. // ===================== 模块切换 =====================
  559. document.getElementById('moduleTabs').addEventListener('click', (e) => {
  560. if (!e.target.classList.contains('module-tab')) return;
  561. document.querySelectorAll('#moduleTabs .module-tab').forEach(t => t.classList.remove('active'));
  562. e.target.classList.add('active');
  563. renderModule(e.target.dataset.module);
  564. });
  565. document.getElementById('adminModuleTabs').addEventListener('click', (e) => {
  566. if (!e.target.classList.contains('module-tab')) return;
  567. document.querySelectorAll('#adminModuleTabs .module-tab').forEach(t => t.classList.remove('active'));
  568. e.target.classList.add('active');
  569. renderModule(e.target.dataset.module);
  570. });
  571. // ===================== 渲染模块面板 =====================
  572. function renderModule(module) {
  573. const panels = document.getElementById('panels');
  574. let html = '';
  575. switch (module) {
  576. // 用户侧模块
  577. case 'auth': html = renderAuthModule(); break;
  578. case 'crawler': html = renderCrawlerModule(); break;
  579. case 'invite': html = renderInviteModule(); break;
  580. case 'coupon': html = renderCouponModule(); break;
  581. case 'membership': html = renderMembershipModule(); break;
  582. case 'payment': html = renderPaymentModule(); break;
  583. case 'watchlist': html = renderWatchlistModule(); break;
  584. case 'trial-quota': html = renderTrialQuotaModule(); break;
  585. case 'upload': html = renderUploadModule(); break;
  586. case 'search': html = renderSearchModule(); break;
  587. case 'platform-account': html = renderPlatformAccountModule(); break;
  588. case 'business-license': html = renderBusinessLicenseModule(); break;
  589. case 'platform-config': html = renderPlatformConfigModule(); break;
  590. case 'search-record': html = renderSearchRecordModule(); break;
  591. case 'platform-session': html = renderPlatformSessionModule(); break;
  592. // 运营侧模块
  593. case 'admin-membership': html = renderAdminMembershipModule(); break;
  594. case 'admin-invite': html = renderAdminInviteModule(); break;
  595. case 'admin-crawler': html = renderAdminCrawlerModule(); break;
  596. case 'admin-coupon': html = renderAdminCouponModule(); break;
  597. case 'admin-license': html = renderAdminLicenseModule(); break;
  598. case 'admin-payment': html = renderAdminPaymentModule(); break;
  599. case 'admin-anxin': html = renderAdminAnxinModule(); break;
  600. case 'admin-trial-quota': html = renderAdminTrialQuotaModule(); break;
  601. case 'admin-user': html = renderAdminUserModule(); break;
  602. case 'admin-audit': html = renderAdminAuditModule(); break;
  603. }
  604. panels.innerHTML = html;
  605. // 修复含引号的默认值:从 data-raw 属性恢复正确的 input value
  606. panels.querySelectorAll('input[data-raw]').forEach(input => {
  607. input.value = input.getAttribute('data-raw');
  608. input.removeAttribute('data-raw');
  609. });
  610. }
  611. function toValidatorName(id) {
  612. const parts = id.split('-');
  613. let camel = parts[0];
  614. for (let i = 1; i < parts.length; i++) {
  615. camel += parts[i].charAt(0).toUpperCase() + parts[i].slice(1);
  616. }
  617. return 'validate_' + camel;
  618. }
  619. // ==================== 字段说明数据 ====================
  620. const FIELD_DESC = {
  621. 'resultWrapper': [
  622. ['code', 'Integer', '业务状态码(200=成功)'],
  623. ['message', 'String', '提示信息(如:操作成功)'],
  624. ['data', 'Object/Array/null', '返回数据(详见下方字段)'],
  625. ['timestamp', 'Long', '响应时间戳(毫秒级)']
  626. ],
  627. 'loginResponse': [
  628. ['userId', 'Long', '用户ID'],
  629. ['accessToken', 'String', 'JWT访问令牌'],
  630. ['refreshToken', 'String', '刷新令牌'],
  631. ['expiresIn', 'Long', '令牌过期时间(秒)'],
  632. ['userInfo', 'Object', '用户信息(嵌套对象,见下方子字段)'],
  633. ['userInfo.id', 'Long', '用户ID(主键)'],
  634. ['userInfo.userId', 'Long', '用户ID(兼容字段=id)'],
  635. ['userInfo.phone', 'String', '手机号(脱敏 187****6917)'],
  636. ['userInfo.nickname', 'String', '昵称(如:用户6917)'],
  637. ['userInfo.avatar', 'String', '头像URL(可能为null)'],
  638. ['userInfo.wechatBound', 'Boolean', '是否已绑定微信'],
  639. ['userInfo.levelInfo', 'Object', '等级信息(嵌套对象,见下方子字段)'],
  640. ['userInfo.levelInfo.id', 'Long', '等级ID'],
  641. ['userInfo.levelInfo.levelName', 'String', '等级名称(如:普通会员)'],
  642. ['userInfo.levelInfo.levelCode', 'String', '等级编码(如:PLUS)'],
  643. ['userInfo.levelInfo.levelWeight', 'Integer', '等级权重(越高越好)'],
  644. ['userInfo.levelInfo.crawlerQuota', 'Integer', '每日爬虫配额'],
  645. ['userInfo.levelInfo.monthlyQuota', 'Integer', '每月爬虫配额'],
  646. ['userInfo.levelInfo.maxConcurrent', 'Integer', '最大并发数'],
  647. ['userInfo.levelInfo.description', 'String', '等级描述'],
  648. ['userInfo.levelInfo.version', 'String', '配置版本号(可能为null)'],
  649. ['userInfo.levelInfo.membershipExpireAt', 'DateTime', '会员到期时间(null=永久或无会员)'],
  650. ['userInfo.levelInfo.membershipDaysRemaining', 'Integer', '会员剩余天数(-1=永久,0=已过期)'],
  651. ['userInfo.levelInfo.pricingPlans', 'Array', '当前等级套餐定价方案列表'],
  652. ['userInfo.levelInfo.pricingPlans[].planCode', 'String', '方案编码(MONTHLY_PLUS/YEARLY_PLUS等)'],
  653. ['userInfo.levelInfo.pricingPlans[].planName', 'String', '方案名称(普通会员·月卡)'],
  654. ['userInfo.levelInfo.pricingPlans[].price', 'BigDecimal', '价格(元,如9.90)'],
  655. ['userInfo.levelInfo.pricingPlans[].originalPrice', 'BigDecimal', '原价(元,如19.90)'],
  656. ['userInfo.levelInfo.pricingPlans[].durationDays', 'Integer', '会员有效天数(30/365)'],
  657. ['userInfo.levelInfo.pricingPlans[].description', 'String', '方案描述'],
  658. ['userInfo.createTime', 'DateTime', '注册时间'],
  659. ['userInfo.loginSource', 'String', '登录来源(SMS/PASSWORD/WECHAT,可能为null)'],
  660. ['userInfo.levelCode', 'String', '等级编码(冗余字段)'],
  661. ['userInfo.version', 'String', '等级版本(冗余字段,可能为null)'],
  662. ['needBindPhone', 'Boolean', '是否需要绑定手机号'],
  663. ['tempToken', 'String', '临时令牌(用于绑定手机号,通常为null)'],
  664. ['newUser', 'Boolean', '是否为新注册用户'],
  665. ['kickedPreviousSession', 'Boolean', '是否踢出了旧设备(true=旧设备已被强制下线)'],
  666. ['kickedDeviceInfo', 'String', '被踢出设备信息(如 "Windows / 192.168.1.1")']
  667. ],
  668. 'userInfo': [
  669. ['id', 'Long', '用户ID(主键)'],
  670. ['userId', 'Long', '用户ID(兼容字段=id)'],
  671. ['phone', 'String', '手机号(脱敏 187****6917)'],
  672. ['nickname', 'String', '昵称(如:用户6917)'],
  673. ['avatar', 'String', '头像URL(可能为null)'],
  674. ['wechatBound', 'Boolean', '是否已绑定微信'],
  675. ['levelInfo', 'Object', '等级信息(嵌套对象,见下方子字段)'],
  676. ['levelInfo.id', 'Long', '等级ID'],
  677. ['levelInfo.levelName', 'String', '等级名称(如:普通会员)'],
  678. ['levelInfo.levelCode', 'String', '等级编码(如:PLUS)'],
  679. ['levelInfo.levelWeight', 'Integer', '等级权重(越高越好)'],
  680. ['levelInfo.crawlerQuota', 'Integer', '每日爬虫配额'],
  681. ['levelInfo.monthlyQuota', 'Integer', '每月爬虫配额'],
  682. ['levelInfo.maxConcurrent', 'Integer', '最大并发数'],
  683. ['levelInfo.description', 'String', '等级描述'],
  684. ['levelInfo.version', 'String', '配置版本号(可能为null)'],
  685. ['levelInfo.membershipExpireAt', 'DateTime', '会员到期时间(null=永久或无会员)'],
  686. ['levelInfo.membershipDaysRemaining', 'Integer', '会员剩余天数(-1=永久,0=已过期)'],
  687. ['levelInfo.pricingPlans', 'Array', '当前等级套餐定价方案列表'],
  688. ['levelInfo.pricingPlans[].planCode', 'String', '方案编码(MONTHLY_PLUS/YEARLY_PLUS等)'],
  689. ['levelInfo.pricingPlans[].planName', 'String', '方案名称(普通会员·月卡)'],
  690. ['levelInfo.pricingPlans[].price', 'BigDecimal', '价格(元,如9.90)'],
  691. ['levelInfo.pricingPlans[].originalPrice', 'BigDecimal', '原价(元,如19.90)'],
  692. ['levelInfo.pricingPlans[].durationDays', 'Integer', '会员有效天数(30/365)'],
  693. ['levelInfo.pricingPlans[].description', 'String', '方案描述'],
  694. ['membershipLevel', 'String', '会员等级(PLUS/PRO/ULTRA)'],
  695. ['membershipLevelName', 'String', '会员等级中文名'],
  696. ['inviterId', 'Long', '邀请人ID(null=未被邀请)'],
  697. ['inviterNickname', 'String', '邀请人昵称(null=未被邀请)'],
  698. ['hasBoundInviteCode', 'Boolean', '是否已绑定邀请码'],
  699. ['createTime', 'DateTime', '注册时间'],
  700. ['loginSource', 'String', '登录来源(SMS/PASSWORD/WECHAT)'],
  701. ['levelCode', 'String', '等级编码(来自levelInfo)']
  702. ],
  703. 'crawlerStatus': [
  704. ['levelName', 'String', '等级名称(如:普通用户)'],
  705. ['levelCode', 'String', '等级编码(如:NORMAL)'],
  706. ['todayUsed', 'Integer', '今日已用次数'],
  707. ['todayQuota', 'Integer', '今日总配额(等级+额外)'],
  708. ['todayRemaining', 'Integer', '今日剩余次数'],
  709. ['monthUsed', 'Integer', '本月已用次数'],
  710. ['monthQuota', 'Integer', '本月总配额(等级+额外)'],
  711. ['monthRemaining', 'Integer', '本月剩余次数'],
  712. ['baseDailyQuota', 'Integer', '等级基础每日配额'],
  713. ['baseMonthlyQuota', 'Integer', '等级基础每月配额'],
  714. ['extraQuota', 'Integer', '额外配额总额(三种来源之和)'],
  715. ['extraQuotaBreakdown', 'Array', '额外配额来源明细(见下方)'],
  716. ['extraQuotaBreakdown[].sourceType', 'String', '来源:INVITE/PURCHASE/ADMIN'],
  717. ['extraQuotaBreakdown[].sourceName', 'String', '来源名称(邀请奖励/购买会员/运营发放)'],
  718. ['extraQuotaBreakdown[].available', 'Integer', '该来源可用次数'],
  719. ['extraQuotaBreakdown[].total', 'Integer', '该来源累计获得'],
  720. ['extraQuotaBreakdown[].used', 'Integer', '该来源已使用'],
  721. ['maxConcurrent', 'Integer', '最大并发数'],
  722. ['hasCoupon', 'Boolean', '是否有可用优惠券'],
  723. ['availableCouponCount', 'Integer', '可用优惠券数量']
  724. ],
  725. 'crawlerLog': [
  726. ['id', 'Long', '记录ID'],
  727. ['userId', 'Long', '用户ID'],
  728. ['usageDate', 'Date', '使用日期'],
  729. ['usageCount', 'Integer', '本次使用次数'],
  730. ['platform', 'String', '爬取平台代码'],
  731. ['source', 'String', '消耗来源(QUOTA/COUPON)'],
  732. ['remark', 'String', '备注'],
  733. ['createTime', 'DateTime', '创建时间']
  734. ],
  735. 'inviteCode': [
  736. ['code', 'String', '邀请码(8位)'],
  737. ['inviteLink', 'String', '完整邀请链接'],
  738. ['inviterNickname', 'String', '邀请人昵称'],
  739. ['usedCount', 'Integer', '已使用次数(注册)'],
  740. ['clickCount', 'Integer', '链接被打开次数'],
  741. ['maxUses', 'Integer', '最大使用次数'],
  742. ['expireTime', 'DateTime', '过期时间'],
  743. ['status', 'Integer', '状态'],
  744. ['createTime', 'DateTime', '创建时间'],
  745. ['shareTitle', 'String', '分享标题'],
  746. ['shareDescription', 'String', '分享描述'],
  747. ['copyText', 'String', '一键复制文本(格式:我是{昵称},在这里发现了一个药店采购神器——{appName}!...下载链接{inviteLink})'],
  748. ['rewardDescription', 'String', '奖励说明'],
  749. ['downloadUrl', 'String', '下载链接(与 inviteLink 一致,访问 /api/invite/{code} 服务端按设备流式返回安装包)']
  750. ],
  751. 'myInviter': [
  752. ['invited', 'Boolean', '是否被邀请过'],
  753. ['inviterId', 'Long', '邀请人ID(未邀请时为null)'],
  754. ['inviterNickname', 'String', '邀请人昵称'],
  755. ['inviterPhone', 'String', '邀请人手机号(脱敏)'],
  756. ['inviterAvatar', 'String', '邀请人头像'],
  757. ['pharmacyName', 'String', '邀请人药店名称'],
  758. ['inviteCode', 'String', '邀请码'],
  759. ['inviteTime', 'DateTime', '邀请时间']
  760. ],
  761. 'inviteStats': [
  762. ['myInviteCode', 'String', '我的邀请码'],
  763. ['inviteLink', 'String', '邀请链接'],
  764. ['clickedCount', 'Integer', '链接被打开总次数'],
  765. ['totalInvited', 'Integer', '已邀请人数'],
  766. ['registeredCount', 'Integer', '已注册人数'],
  767. ['totalReward', 'Integer', '累计获得奖励会员月数'],
  768. ['summaryText', 'String', '摘要文本'],
  769. ['todayInvited', 'Integer', '今日已邀请人数'],
  770. ['todayRemaining', 'Integer', '今日剩余可邀次数'],
  771. ['rewardPerInvite', 'Integer', '每次邀请奖励会员月数'],
  772. ['crawlerTodayRemaining', 'Integer', '今日爬虫剩余配额(与爬虫状态页一致)'],
  773. ['crawlerQuotaFromInvites', 'Integer', '邀请贡献的额外爬虫配额']
  774. ],
  775. 'inviteReward': [
  776. ['inviterId', 'Long', '邀请人ID'],
  777. ['inviterNickname', 'String', '邀请人昵称'],
  778. ['inviteeId', 'Long', '被邀请人ID'],
  779. ['inviteeNickname', 'String', '被邀请人昵称'],
  780. ['registered', 'Integer', '是否已注册(0仅点击/1已注册)'],
  781. ['registeredText', 'String', '注册状态文本'],
  782. ['rewardCount', 'Integer', '奖励会员月数'],
  783. ['inviteTime', 'String', '邀请时间']
  784. ],
  785. 'invitePage': [
  786. ['inviterNickname', 'String', '邀请人昵称'],
  787. ['inviterAvatar', 'String', '邀请人头像'],
  788. ['inviteCode', 'String', '邀请码'],
  789. ['channel', 'String', '渠道(windows/app/wechat/miniapp等,默认windows)'],
  790. ['openType', 'String', '打开方式(download/redirect等)'],
  791. ['appName', 'String', '应用名称'],
  792. ['landingTitle', 'String', '落地页标题'],
  793. ['landingDesc', 'String', '落地页描述'],
  794. ['appDownloadUrl', 'String', 'Windows下载入口URL(channel=windows/app),访问 /api/invite/{code} 会根据设备流式返回对应安装包'],
  795. ['miniappPath', 'String', '小程序路径'],
  796. ['miniappAppId', 'String', '小程序AppID'],
  797. ['wechatRedirectUrl', 'String', '微信跳转URL'],
  798. ['dingtalkAppId', 'String', '钉钉AppID'],
  799. ['feishuAppId', 'String', '飞书AppID'],
  800. ['rewardPerRegister', 'Integer', '每次注册奖励会员月数'],
  801. ['autoFillSupported', 'Boolean', '是否支持自动填入'],
  802. ['urlScheme', 'String', 'URL Scheme'],
  803. ['deepLink', 'String', '深度链接'],
  804. ['instructionText', 'String', '提示文案']
  805. ],
  806. 'couponIssue': [
  807. ['data', 'Integer', '成功发放的优惠券数量(如:2表示2张)']
  808. ],
  809. 'coupon': [
  810. ['id', 'Long', '优惠券ID'],
  811. ['couponName', 'String', '优惠券名称'],
  812. ['type', 'String', '类型(CRAWLER等)'],
  813. ['value', 'Integer', '券面值(次数)'],
  814. ['status', 'Integer', '状态(0未用/1已用/2过期)'],
  815. ['expireTime', 'DateTime', '过期时间'],
  816. ['usedTime', 'DateTime', '使用时间'],
  817. ['source', 'String', '来源'],
  818. ['createTime', 'DateTime', '领取时间']
  819. ],
  820. 'membership': [
  821. ['id', 'Long', '会员记录ID'],
  822. ['userId', 'Long', '用户ID'],
  823. ['phone', 'String', '用户手机号'],
  824. ['nickname', 'String', '用户昵称'],
  825. ['level', 'String', '会员等级(PLUS/PRO/ULTRA)'],
  826. ['levelName', 'String', '等级中文名'],
  827. ['source', 'String', '来源(ADMIN_GRANT/RECHARGE/SYSTEM)'],
  828. ['effectiveFrom', 'DateTime', '生效开始时间'],
  829. ['effectiveTo', 'DateTime', '生效结束时间(NULL表示永久)'],
  830. ['status', 'Integer', '状态(1有效/0过期停用)'],
  831. ['remark', 'String', '备注'],
  832. ['createTime', 'DateTime', '创建时间']
  833. ],
  834. 'membershipBenefit': [
  835. ['benefitType', 'String', '权益类型(DAILY_QUOTA/MONTHLY_QUOTA/MAX_CONCURRENT)'],
  836. ['benefitTypeName', 'String', '权益类型中文名'],
  837. ['benefitValue', 'Integer', '权益数值'],
  838. ['description', 'String', '权益说明']
  839. ],
  840. 'levelResponse': [
  841. ['id', 'Long', '等级ID'],
  842. ['levelName', 'String', '等级名称'],
  843. ['levelCode', 'String', '等级编码'],
  844. ['levelWeight', 'Integer', '等级权重(越高越好)'],
  845. ['crawlerQuota', 'Integer', '每日爬虫配额'],
  846. ['monthlyQuota', 'Integer', '每月爬虫配额'],
  847. ['maxConcurrent', 'Integer', '最大并发数'],
  848. ['description', 'String', '等级描述'],
  849. ['version', 'String', '配置版本号'],
  850. ['membershipExpireAt', 'DateTime', '会员到期时间(null=永久或无会员)'],
  851. ['membershipDaysRemaining', 'Integer', '会员剩余天数(-1=永久,0=已过期)'],
  852. ['pricingPlans', 'Array', '当前等级套餐定价方案列表'],
  853. ['pricingPlans[].planCode', 'String', '方案编码(MONTHLY_PLUS/YEARLY_PLUS等)'],
  854. ['pricingPlans[].planName', 'String', '方案名称(普通会员·月卡)'],
  855. ['pricingPlans[].price', 'BigDecimal', '价格(元,如9.90)'],
  856. ['pricingPlans[].originalPrice', 'BigDecimal', '原价(元,如19.90)'],
  857. ['pricingPlans[].durationDays', 'Integer', '会员有效天数(30/365)'],
  858. ['pricingPlans[].description', 'String', '方案描述']
  859. ],
  860. 'platformAccount': [
  861. ['id', 'Long', '记录ID'],
  862. ['userId', 'Long', '用户ID'],
  863. ['platformCode', 'String', '平台代码'],
  864. ['platformName', 'String', '平台名称'],
  865. ['accountEncrypted', 'String', '加密账号'],
  866. ['passwordEncrypted', 'String', '加密密码'],
  867. ['enabled', 'Boolean', '是否启用'],
  868. ['lastVerifyTime', 'DateTime', '最后验证时间'],
  869. ['verifyStatus', 'String', '验证状态(VERIFIED/FAILED/PENDING)'],
  870. ['failReason', 'String', '失败原因'],
  871. ['createTime', 'DateTime', '创建时间'],
  872. ['updateTime', 'DateTime', '更新时间']
  873. ],
  874. 'businessLicense': [
  875. ['id', 'Long', '记录ID'],
  876. ['userId', 'Long', '用户ID'],
  877. ['licenseImageUrl', 'String', '执照图片URL'],
  878. ['creditCode', 'String', '统一社会信用代码'],
  879. ['pharmacyName', 'String', '药店名称'],
  880. ['legalPerson', 'String', '法人代表'],
  881. ['registeredAddress', 'String', '注册地址'],
  882. ['businessScope', 'String', '经营范围'],
  883. ['validFrom', 'Date', '有效期开始'],
  884. ['validTo', 'Date', '有效期结束'],
  885. ['reviewStatus', 'String', '审核状态(PENDING/APPROVED/REJECTED)'],
  886. ['reviewerId', 'Long', '审核人ID'],
  887. ['reviewTime', 'DateTime', '审核时间'],
  888. ['rejectReason', 'String', '驳回原因'],
  889. ['showVerifiedBadge', 'Boolean', '是否展示认证徽章'],
  890. ['remark', 'String', '备注'],
  891. ['createTime', 'DateTime', '创建时间'],
  892. ['updateTime', 'DateTime', '更新时间']
  893. ],
  894. 'platformConfig': [
  895. ['id', 'Long', '配置ID'],
  896. ['platformCode', 'String', '平台代码(如yaoshibang)'],
  897. ['platformName', 'String', '平台名称'],
  898. ['enabled', 'Boolean', '是否启用'],
  899. ['requiresRecharge', 'Boolean', '是否需要充值'],
  900. ['queryCost', 'Integer', '每次查询消耗'],
  901. ['priority', 'Integer', '优先级'],
  902. ['underMaintenance', 'Boolean', '是否维护中'],
  903. ['maintenanceNotice', 'String', '维护公告'],
  904. ['description', 'String', '平台描述'],
  905. ['officialUrl', 'String', '官方网址'],
  906. ['bindingInstructions', 'String', '绑定说明'],
  907. ['sortOrder', 'Integer', '排序'],
  908. ['createTime', 'DateTime', '创建时间'],
  909. ['updateTime', 'DateTime', '更新时间'],
  910. ['createdBy', 'String', '创建人'],
  911. ['updatedBy', 'String', '更新人']
  912. ],
  913. 'trialQuota': [
  914. ['id', 'Long', '配置ID'],
  915. ['configName', 'String', '配置名称'],
  916. ['trialDays', 'Integer', '体验天数'],
  917. ['dailyQueryLimit', 'Integer', '每日查询限制'],
  918. ['monthlyWatchlistLimit', 'Integer', '月关注限制'],
  919. ['enabled', 'Boolean', '是否启用'],
  920. ['configType', 'String', '配置类型(DEFAULT/CUSTOM)'],
  921. ['applicableUserType', 'String', '适用用户类型(NEW_USER/ALL)'],
  922. ['effectiveFrom', 'DateTime', '生效开始时间'],
  923. ['effectiveTo', 'DateTime', '生效结束时间'],
  924. ['maxUsers', 'Integer', '最大用户数'],
  925. ['currentUsers', 'Integer', '当前用户数'],
  926. ['priority', 'Integer', '优先级'],
  927. ['description', 'String', '描述'],
  928. ['remark', 'String', '备注'],
  929. ['createdBy', 'String', '创建人'],
  930. ['updatedBy', 'String', '更新人'],
  931. ['createTime', 'DateTime', '创建时间'],
  932. ['updateTime', 'DateTime', '更新时间']
  933. ],
  934. 'inviteConfig': [
  935. ['id', 'Long', '配置ID'],
  936. ['rewardCrawlerCount', 'Integer', '每次邀请奖励爬虫次数(历史字段)'],
  937. ['rewardType', 'String', '奖励类型(MEMBERSHIP等)'],
  938. ['rewardMonths', 'Integer', '邀请人奖励会员时长(月)'],
  939. ['maxDailyReward', 'Integer', '每日最大奖励次数'],
  940. ['maxTotalInvites', 'Integer', '单个账号总邀请上限(默认30)'],
  941. ['inviteCodeExpireDays', 'Integer', '邀请码过期天数'],
  942. ['maxInvitePerDay', 'Integer', '每日最大邀请数'],
  943. ['appDownloadUrl', 'String', 'APP下载地址'],
  944. ['miniappPath', 'String', '小程序路径'],
  945. ['miniappAppId', 'String', '小程序AppID'],
  946. ['wechatRedirectUrl', 'String', '微信跳转URL'],
  947. ['dingtalkAppId', 'String', '钉钉AppID'],
  948. ['feishuAppId', 'String', '飞书AppID'],
  949. ['landingTitle', 'String', '落地页标题(支持{inviter}{pharmacy}占位符)'],
  950. ['landingDesc', 'String', '落地页描述'],
  951. ['appName', 'String', '应用名称'],
  952. ['shareTitleTemplate', 'String', '分享标题模板(支持{inviter}{appName})'],
  953. ['copyTextTemplate', 'String', '一键复制文案模板(支持{inviter}{appName}{inviteLink}{code})'],
  954. ['rewardDescription', 'String', '奖励说明文案(支持{rewardMonths})'],
  955. ['summaryTextTemplate', 'String', '统计摘要模板(支持{totalInvited}{registeredCount}{totalReward})'],
  956. ['instructionWindows', 'String', 'Windows渠道提示文案(支持{code})'],
  957. ['instructionApp', 'String', 'App渠道提示文案(支持{code})'],
  958. ['instructionMiniapp', 'String', '小程序渠道提示文案(支持{code})'],
  959. ['instructionWechat', 'String', '微信渠道提示文案(支持{code})'],
  960. ['instructionDingtalk', 'String', '钉钉渠道提示文案(支持{code}{appName})'],
  961. ['instructionFeishu', 'String', '飞书渠道提示文案(支持{code}{appName})'],
  962. ['instructionDefault', 'String', '默认渠道提示文案(支持{code})'],
  963. ['status', 'String', '状态(ACTIVE/INACTIVE)'],
  964. ['remark', 'String', '备注'],
  965. ['createTime', 'DateTime', '创建时间'],
  966. ['updateTime', 'DateTime', '更新时间']
  967. ],
  968. 'quotaGrant': [
  969. ['id', 'Long', '发放记录ID'],
  970. ['userId', 'Long', '用户ID'],
  971. ['grantType', 'String', '来源类型(ADMIN/INVITE/PURCHASE)'],
  972. ['quotaCount', 'Integer', '发放配额数'],
  973. ['usedCount', 'Integer', '已使用数'],
  974. ['expireTime', 'DateTime', '过期时间'],
  975. ['sourceUserId', 'Long', '来源用户ID(邀请人)'],
  976. ['remark', 'String', '备注'],
  977. ['status', 'String', '状态(ACTIVE/EXPIRED/USED_UP)'],
  978. ['createTime', 'DateTime', '创建时间'],
  979. ['updateTime', 'DateTime', '更新时间']
  980. ],
  981. 'crawlerUserDetail': [
  982. ['userId', 'Long', '用户ID'],
  983. ['nickname', 'String', '昵称'],
  984. ['phone', 'String', '手机号'],
  985. ['todayTotalUsage', 'Integer', '今日总使用次数'],
  986. ['monthlyTotalUsage', 'Integer', '本月总使用次数'],
  987. ['platformStats', 'Array', '各平台使用统计(见下方)'],
  988. ['platformStats[].platformCode', 'String', '平台代码'],
  989. ['platformStats[].platformName', 'String', '平台名称'],
  990. ['platformStats[].todayUsage', 'Integer', '今日使用'],
  991. ['platformStats[].monthlyUsage', 'Integer', '本月使用'],
  992. ['platformStats[].totalUsage', 'Integer', '累计使用']
  993. ],
  994. 'conversionStats': [
  995. ['inviteCode', 'String', '邀请码'],
  996. ['clickedCount', 'Integer', '链接点击次数'],
  997. ['totalInvited', 'Integer', '总邀请数'],
  998. ['registeredCount', 'Integer', '注册人数'],
  999. ['pendingCount', 'Integer', '待审核数'],
  1000. ['conversionRate', 'Double', '转化率(%)'],
  1001. ['totalReward', 'Integer', '总奖励数']
  1002. ],
  1003. 'auditLog': [
  1004. ['id', 'Long', '日志ID'],
  1005. ['operatorId', 'Long', '操作人ID'],
  1006. ['operatorName', 'String', '操作人姓名'],
  1007. ['operatorRole', 'String', '操作人角色'],
  1008. ['operationTime', 'DateTime', '操作时间'],
  1009. ['ipAddress', 'String', '操作IP'],
  1010. ['userAgent', 'String', '浏览器UA'],
  1011. ['module', 'String', '模块(USER/INVITE/CRAWLER等)'],
  1012. ['operationType', 'String', '操作类型(CREATE/UPDATE/DELETE等)'],
  1013. ['description', 'String', '操作描述'],
  1014. ['targetType', 'String', '目标类型'],
  1015. ['targetId', 'Long', '目标ID'],
  1016. ['targetIdentifier', 'String', '目标标识'],
  1017. ['beforeData', 'Object', '变更前数据'],
  1018. ['afterData', 'Object', '变更后数据'],
  1019. ['result', 'String', '操作结果(SUCCESS/FAILED)'],
  1020. ['errorMessage', 'String', '错误信息'],
  1021. ['requestUrl', 'String', '请求URL'],
  1022. ['requestMethod', 'String', '请求方法'],
  1023. ['requestParams', 'String', '请求参数'],
  1024. ['responseCode', 'Integer', '响应码'],
  1025. ['duration', 'Long', '耗时(ms)'],
  1026. ['remark', 'String', '备注'],
  1027. ['isSensitive', 'Boolean', '是否敏感操作'],
  1028. ['createTime', 'DateTime', '创建时间']
  1029. ],
  1030. 'inviteLinkResolve': [
  1031. ['inviteCode', 'String', '邀请码'],
  1032. ['inviteLink', 'String', '完整邀请链接'],
  1033. ['inviterNickname', 'String', '邀请人昵称'],
  1034. ['inviterAvatar', 'String', '邀请人头像'],
  1035. ['appName', 'String', '应用名称'],
  1036. ['landingTitle', 'String', '落地页标题'],
  1037. ['landingDesc', 'String', '落地页描述']
  1038. ],
  1039. 'anxinCard': [
  1040. ['id', 'Long', '本地记录ID'],
  1041. ['planId', 'Long', '关联支付方案ID'],
  1042. ['planName', 'String', '支付方案名称'],
  1043. ['cardId', 'String', '支付宝卡模板ID'],
  1044. ['cardName', 'String', '卡模板名称'],
  1045. ['cardType', 'String', '卡类型(PERIOD_PAY=周期卡)'],
  1046. ['periodAmount', 'BigDecimal', '每期金额(元)'],
  1047. ['periodDays', 'Integer', '周期天数'],
  1048. ['totalPeriods', 'Integer', '总期数'],
  1049. ['status', 'String', '状态(PENDING/EFFECTIVE/INVALID/FROZEN)'],
  1050. ['appointmentUrl', 'String', '预约链接(小程序购买入口)'],
  1051. ['createTime', 'DateTime', '创建时间'],
  1052. ['updateTime', 'DateTime', '更新时间']
  1053. ],
  1054. 'agreementSignResponse': [
  1055. ['agreementId', 'Long', '本地协议ID'],
  1056. ['externalAgreementNo', 'String', '商家自定义协议号(AG开头)'],
  1057. ['signUrl', 'String', '签约页URL(前端跳转/展示)'],
  1058. ['qrContent', 'String', '二维码内容(扫码签约)'],
  1059. ['hint', 'String', '提示文案']
  1060. ],
  1061. 'agreementResponse': [
  1062. ['id', 'Long', '协议ID'],
  1063. ['userId', 'Long', '用户ID'],
  1064. ['planId', 'Long', '支付方案ID'],
  1065. ['planName', 'String', '方案名称'],
  1066. ['channel', 'String', '渠道(WECHAT)'],
  1067. ['periodType', 'String', '周期类型(MONTH/YEAR)'],
  1068. ['periodAmount', 'BigDecimal', '每期扣款金额'],
  1069. ['status', 'String', '状态(PENDING/SIGNED/UNSIGNED/FAILED)'],
  1070. ['agreementNo', 'String', '第三方协议号'],
  1071. ['signTime', 'DateTime', '签约时间'],
  1072. ['unsignTime', 'DateTime', '解约时间'],
  1073. ['nextDeductTime', 'DateTime', '下次扣款时间'],
  1074. ['lastDeductTime', 'DateTime', '上次扣款时间'],
  1075. ['consecutiveFailCount', 'Integer', '连续失败次数(达3次自动解约)'],
  1076. ['unsignReason', 'String', '解约原因'],
  1077. ['createTime', 'DateTime', '创建时间']
  1078. ],
  1079. 'searchRecordResponse': [
  1080. ['id', 'Long', '记录ID(雪花算法)'],
  1081. ['drugName', 'String', '药品名称'],
  1082. ['searchedAt', 'DateTime', '搜索时间(yyyy-MM-dd HH:mm:ss)'],
  1083. ['statusText', 'String', '状态文案(如:已返回 2 个平台的可比价结果)'],
  1084. ['platformCount', 'Integer', '涉及平台数量(后端自动从platformResults计算)'],
  1085. ['quoteCount', 'Integer', '总报价条数(后端自动从platformResults计算)'],
  1086. ['platformResults', 'Object/Array', '平台搜索结果(结构化JSON对象,仅详情接口返回,列表接口为null)'],
  1087. ['searchLog', 'Object/null', '搜索日志(仅详情接口返回,列表接口为null)']
  1088. ],
  1089. };
  1090. function renderFieldDesc(descKey) {
  1091. const fields = FIELD_DESC[descKey];
  1092. if (!fields) return '';
  1093. let rows = fields.map(f => `<tr><td><code>${f[0]}</code></td><td>${f[1]}</td><td>${f[2]}</td></tr>`).join('');
  1094. return `<div class="field-desc-box">
  1095. <div class="field-desc-title">返回字段说明</div>
  1096. <table class="field-desc-table"><tr><th>字段名</th><th>类型</th><th>说明</th></tr>${rows}</table>
  1097. </div>`;
  1098. }
  1099. // ==================== 各模块HTML ====================
  1100. function testSection(id, title, method, methodClass, path, authTag, authClass, fields, hasValidator, descKey, desc) {
  1101. const authLabel = authTag === '需认证' ? '需认证' : '公开';
  1102. const ac = authTag === '需认证' ? 'auth-required' : 'auth-public';
  1103. const side = (path.includes('/api/admin') || path === '/api/coupon/issue') ? 'admin' : 'user';
  1104. const sideClass = side === 'admin' ? 'side-admin' : 'side-user';
  1105. const sideLabel = side === 'admin' ? '运营侧' : '用户侧';
  1106. let fieldsHtml = '';
  1107. if (fields) {
  1108. fieldsHtml = '<div class="form-grid">';
  1109. fields.forEach(f => {
  1110. const escapedDefault = (f.default || '').replace(/"/g, '&quot;');
  1111. const escapedPlaceholder = (f.placeholder || '').replace(/"/g, '&quot;');
  1112. const hasRaw = f.default && f.default.includes('"');
  1113. fieldsHtml += `<div class="form-group"><label>${f.label}</label><input type="text" id="${id}-${f.name}" placeholder="${escapedPlaceholder}" value="${escapedDefault}"${hasRaw ? ` data-raw="${escapedDefault}"` : ''}></div>`;
  1114. });
  1115. fieldsHtml += '</div>';
  1116. }
  1117. const descHtml = desc ? `<div class="section-desc">${desc}</div>` : '';
  1118. const fieldDescHtml = descKey ? renderFieldDesc(descKey) : '';
  1119. return `
  1120. <div class="test-section">
  1121. <div class="section-title">
  1122. <span class="method method-${methodClass}">${method}</span>
  1123. <span class="path">${path}</span>
  1124. <span class="side-tag ${sideClass}">${sideLabel}</span>
  1125. <span class="${ac} auth-tag">${authLabel}</span>
  1126. </div>
  1127. ${descHtml}
  1128. ${fieldsHtml}
  1129. <button class="btn btn-primary" onclick="runTest('${id}', '${method}', '${path}', ${buildBody(id, fields)}, ${authTag === '公开' ? 'false' : 'true'}, ${hasValidator ? `'${toValidatorName(id)}'` : 'null'})">执行测试</button>
  1130. <div class="response-box" id="${id}-box" style="display:none;">
  1131. <div class="response-header">
  1132. <span class="response-title">响应 <span id="${id}-duration" style="color:#999;font-weight:400;"></span></span>
  1133. <span class="status-badge" id="${id}-status"></span>
  1134. </div>
  1135. <div class="response-content" id="${id}-raw"></div>
  1136. ${fieldDescHtml}
  1137. </div>
  1138. </div>`;
  1139. }
  1140. function buildBody(id, fields) {
  1141. if (!fields || fields.length === 0) return 'null';
  1142. let parts = [];
  1143. fields.forEach(f => {
  1144. const forceArray = f.type === 'array';
  1145. const asJson = f.type === 'json';
  1146. const forceString = f.type === 'string';
  1147. if (asJson) {
  1148. parts.push(`&quot;${f.name}&quot;: safeJsonParse(document.getElementById('${id}-${f.name}').value)`);
  1149. } else if (forceString) {
  1150. parts.push(`&quot;${f.name}&quot;: document.getElementById('${id}-${f.name}').value || null`);
  1151. } else {
  1152. parts.push(`&quot;${f.name}&quot;: parseFieldValue(document.getElementById('${id}-${f.name}').value, ${forceArray})`);
  1153. }
  1154. });
  1155. return `{${parts.join(',')}}`;
  1156. }
  1157. /**
  1158. * 智能解析字段值:返回真实类型的JS值(非源代码字符串)
  1159. * 逗号分隔→数组,true/false→布尔,数字(含小数)→数值,否则→字符串
  1160. * @param forceArray 为true时,单个值也包装为数组(如userIds填1→[1])
  1161. */
  1162. function parseFieldValue(val, forceArray) {
  1163. if (val === '' || val === undefined || val === null) return forceArray ? [] : null;
  1164. // 布尔
  1165. if (val === 'true') return forceArray ? [true] : true;
  1166. if (val === 'false') return forceArray ? [false] : false;
  1167. // 逗号分隔的数组
  1168. if (val.includes(',')) {
  1169. const items = val.split(',').map(v => v.trim()).filter(v => v !== '');
  1170. return items.map(v => toNumberIfSafe(v));
  1171. }
  1172. // 数字(整数或小数,如 1, 9.90, 0.5);大整数(雪花ID)保持字符串
  1173. if (/^\d+(\.\d+)?$/.test(val)) return forceArray ? [toNumberIfSafe(val)] : toNumberIfSafe(val);
  1174. // 字符串
  1175. return forceArray ? [val] : val;
  1176. }
  1177. /**
  1178. * 尝试转为Number,但大整数(超出JS安全范围如雪花ID)保持字符串避免精度丢失
  1179. */
  1180. function toNumberIfSafe(v) {
  1181. if (!/^\d+(\.\d+)?$/.test(v)) return v;
  1182. const num = Number(v);
  1183. // 整数且超出安全范围(如雪花ID 2069620495151230978) → 保持字符串
  1184. if (Number.isInteger(num) && !Number.isSafeInteger(num)) return v;
  1185. return num;
  1186. }
  1187. function renderAuthModule() {
  1188. return `
  1189. <div class="batch-buttons">
  1190. <button class="btn btn-warning btn-sm" onclick="runAuthTests()">运行全部认证模块测试</button>
  1191. </div>
  1192. ${testSection('auth-send', '发送验证码', 'POST', 'post', '/api/auth/sms/send', '公开', 'auth-public',
  1193. [{label:'手机号(phone)', name:'phone', placeholder:'18776156917', default:'18776156917'},
  1194. {label:'场景(scene)', name:'scene', placeholder:'login', default:'login'}], true, null, '发送短信验证码,开发环境可能不实际发送')}
  1195. ${testSection('auth-smslogin', '短信登录', 'POST', 'post', '/api/auth/sms/login', '公开', 'auth-public',
  1196. [{label:'手机号(phone)', name:'phone', placeholder:'18776156917', default:'18776156917'},
  1197. {label:'验证码(code)', name:'code', placeholder:'123456', default:'123456'},
  1198. {label:'邀请码(inviteCode,可选)', name:'inviteCode', placeholder:'ABC123', default:''},
  1199. {label:'场景(scene)', name:'scene', placeholder:'login', default:'login'}], true, 'loginResponse', '验证码登录/注册,支持附带邀请码自动绑定邀请关系')}
  1200. ${testSection('auth-pwdlogin', '密码登录', 'POST', 'post', '/api/auth/password/login', '公开', 'auth-public',
  1201. [{label:'手机号(phone)', name:'phone', placeholder:'18776156917', default:'18776156917'},
  1202. {label:'密码(password)', name:'password', placeholder:'your_password', default:'123456'}], true, 'loginResponse', '使用手机号+密码登录,需先通过设置密码接口创建密码')}
  1203. ${testSection('auth-wechat-url', '微信扫码URL', 'GET', 'get', '/api/auth/wechat/url?state=login', '公开', 'auth-public', null, false, null, '获取微信扫码登录跳转URL')}
  1204. ${testSection('auth-wechat-login', '微信登录', 'POST', 'post', '/api/auth/wechat/login', '公开', 'auth-public',
  1205. [{label:'微信code(code)', name:'code', placeholder:'微信授权code', default:'test_wechat_code'},
  1206. {label:'state(state)', name:'state', placeholder:'login', default:'login'}], true, 'loginResponse', '微信扫码登录后回调,传入微信授权code')}
  1207. ${testSection('auth-wechat-bind', '微信绑定手机', 'POST', 'post', '/api/auth/wechat/bind', '需认证', 'auth-required',
  1208. [{label:'手机号(phone)', name:'phone', placeholder:'18776156917', default:'18776156917'},
  1209. {label:'验证码(code)', name:'code', placeholder:'123456', default:'123456'},
  1210. {label:'临时令牌(tempToken)', name:'tempToken', placeholder:'微信登录后获取的tempToken', default:''},
  1211. {label:'邀请码(inviteCode,可选)', name:'inviteCode', placeholder:'ABC123', default:''}], true, 'loginResponse', '已有微信登录的用户绑定手机号')}
  1212. ${testSection('auth-wechat-callback', '微信回调', 'GET', 'get', '/api/auth/wechat/callback?code=test_code', '公开', 'auth-public', null, false, null, '微信扫码登录前端跳转回调')}
  1213. ${testSection('auth-refresh', '刷新Token', 'POST', 'post', '/api/auth/refresh', '公开', 'auth-public',
  1214. [{label:'refreshToken(refreshToken)', name:'refreshToken', placeholder:'输入refreshToken', default:''}], false, 'loginResponse', '使用refreshToken获取新的accessToken(Refresh Token一次性旋转,重用即被踢)')}
  1215. ${testSection('auth-heartbeat', '心跳保活', 'POST', 'post', '/api/auth/heartbeat', '需认证', 'auth-required', null, true, 'loginResponse', '客户端定时调用保持登录态(建议4分钟一次),返回新Token对。旧Token自然过期前仍可用')}
  1216. ${testSection('auth-userinfo', '获取用户信息', 'GET', 'get', '/api/auth/user/info', '需认证', 'auth-required', null, true, 'userInfo', '获取当前登录用户的基本信息')}
  1217. ${testSection('auth-setpwd', '设置密码', 'POST', 'post', '/api/auth/password/set', '需认证', 'auth-required',
  1218. [{label:'旧密码(oldPassword,可选)', name:'oldPassword', placeholder:'首次设置可不填', default:''},
  1219. {label:'新密码(newPassword)', name:'newPassword', placeholder:'new_password', default:'Abc123456'}], false, null, '首次设置密码或修改密码')}
  1220. ${testSection('auth-change-phone', '修改手机号', 'POST', 'post', '/api/auth/phone/change', '需认证', 'auth-required',
  1221. [{label:'新手机号(newPhone)', name:'newPhone', placeholder:'13900139000', default:'13900139000'},
  1222. {label:'验证码(code)', name:'code', placeholder:'123456', default:'123456'}], false, null, '修改绑定手机号,需先点击发送验证码获取新手机验证码')}
  1223. <div style="margin-top:-8px;margin-bottom:14px;">
  1224. <button class="btn btn-primary btn-sm" onclick="sendCodeToField('auth-change-phone-newPhone','change_phone')">发送验证码到新手机</button>
  1225. <span style="font-size:11px;color:#888;margin-left:8px;">开发环境固定验证码: 123456</span>
  1226. </div>`;
  1227. }
  1228. function renderCrawlerModule() {
  1229. return `
  1230. <div class="batch-buttons">
  1231. <button class="btn btn-warning btn-sm" onclick="runCrawlerTests()">运行全部爬虫配额测试</button>
  1232. </div>
  1233. ${testSection('crawler-status', '配额状态', 'GET', 'get', '/api/crawler/status', '需认证', 'auth-required', null, true, 'crawlerStatus', '获取当前用户的爬虫配额使用情况')}
  1234. ${testSection('crawler-consume', '消耗配额', 'POST', 'post', '/api/crawler/consume', '需认证', 'auth-required',
  1235. [{label:'次数(count)', name:'count', placeholder:'1', default:'1'},
  1236. {label:'优先优惠券(useCouponFirst)', name:'useCouponFirst', placeholder:'true/false', default:'false'},
  1237. {label:'平台代码(platform)', name:'platform', placeholder:'yaoshibang 或 yaoshibang,yiyaocheng', default:'yaoshibang', type:'string'}], true, null, '前端启动爬虫前调用,扣减配额次数。platform仅用于日志统计,不做限制,支持逗号分隔传入多平台')}
  1238. ${testSection('crawler-logs', '使用记录', 'GET', 'get', '/api/crawler/logs?days=7', '需认证', 'auth-required', null, true, 'crawlerLog', '查询爬虫使用历史记录')}`;
  1239. }
  1240. function renderInviteModule() {
  1241. return `
  1242. <div class="batch-buttons">
  1243. <button class="btn btn-warning btn-sm" onclick="runInviteTests()">运行全部邀请系统测试</button>
  1244. </div>
  1245. ${testSection('invite-code', '获取邀请码', 'GET', 'get', '/api/invite/code', '需认证', 'auth-required', null, true, 'inviteCode', '获取当前用户的邀请码和邀请链接')}
  1246. ${testSection('invite-stats', '邀请统计', 'GET', 'get', '/api/invite/stats', '需认证', 'auth-required', null, true, 'inviteStats', '获取邀请人数、注册人数、累计奖励等统计')}
  1247. ${testSection('invite-rewards', '邀请记录', 'GET', 'get', '/api/invite/rewards', '需认证', 'auth-required', null, true, 'inviteReward', '获取邀请记录列表,含被邀请人注册状态')}
  1248. ${testSection('invite-extra-quota', '额外配额', 'GET', 'get', '/api/invite/extra-quota', '需认证', 'auth-required', null, false, null, '获取可用额外爬虫次数(邀请奖励+运营发放),返回Integer数值')}
  1249. ${testSection('invite-my-inviter', '我的邀请人', 'GET', 'get', '/api/invite/my-inviter', '需认证', 'auth-required', null, true, 'myInviter', '查询是谁邀请了我(被邀请人视角),未被邀请返回invited=false')}
  1250. ${testSection('invite-bind', '补填邀请码', 'POST', 'post', '/api/invite/bind', '需认证', 'auth-required',
  1251. [{label:'邀请码/链接(inviteCode)', name:'inviteCode', placeholder:'A3K7M9P2 或 https://app.zhijiayun.com/invite/A3K7M9P2', default:''}], true, null, '注册时未填邀请码,注册后30天内可补填。补填后邀请人获赠1个月高级会员时长')}
  1252. ${testSection('invite-page', '落地页', 'GET', 'get', '/api/invite/page/{code}?channel=windows', '公开', 'auth-public',
  1253. [{label:'邀请码(code)', name:'code', placeholder:'输入邀请码', default:''},
  1254. {label:'渠道(channel)', name:'channel', placeholder:'windows/app/wechat/miniapp/dingtalk/feishu', default:'windows'}], true, 'invitePage', '浏览器打开邀请链接时展示的落地页数据,默认渠道为windows(Windows桌面客户端)')}
  1255. ${testSection('invite-link-entry', '邀请链接入口(直接下载)', 'GET', 'get', '/api/invite/{code}', '公开', 'auth-public',
  1256. [{label:'邀请码(code)', name:'code', placeholder:'输入邀请码', default:''}], true, 'invitePage', '浏览器直接访问邀请链接,服务端根据 User-Agent 流式返回对应桌面客户端安装包(Windows x64 -> .exe / macOS ARM64 -> .dmg),响应头 Content-Disposition: attachment,并自动计入点击数')}
  1257. ${testSection('invite-click', '点击追踪', 'POST', 'post', '/api/invite/click/{code}', '公开', 'auth-public',
  1258. [{label:'邀请码(code)', name:'code', placeholder:'输入邀请码', default:''}], false, null, '追踪邀请链接被打开事件')}
  1259. ${testSection('invite-resolve', '解析链接', 'GET', 'get', '/api/invite/resolve-link?link=', '公开', 'auth-public',
  1260. [{label:'链接/邀请码(link)', name:'link', placeholder:'https://app.zhijiayun.com/invite/ABC123 或 ABC123', default:''}], false, 'inviteLinkResolve', '解析邀请链接或纯邀请码,识别出邀请人和邀请码')}`;
  1261. }
  1262. function renderCouponModule() {
  1263. return `
  1264. <div class="batch-buttons">
  1265. <button class="btn btn-warning btn-sm" onclick="runCouponTests()">运行全部优惠券测试</button>
  1266. </div>
  1267. ${testSection('coupon-my', '我的优惠券', 'GET', 'get', '/api/coupon/my?status=0', '需认证', 'auth-required', null, true, 'coupon', '查询当前用户的优惠券列表,可按状态筛选')}
  1268. ${testSection('coupon-available', '可用优惠券', 'GET', 'get', '/api/coupon/my/available?type=CRAWLER', '需认证', 'auth-required', null, true, 'coupon', '查询当前用户可用的优惠券,可按类型筛选')}`;
  1269. }
  1270. function renderMembershipModule() {
  1271. return `
  1272. <div class="batch-buttons">
  1273. <button class="btn btn-warning btn-sm" onclick="runMembershipTests()">运行全部会员等级测试</button>
  1274. </div>
  1275. ${testSection('membership-my', '我的会员', 'GET', 'get', '/api/membership/my', '需认证', 'auth-required', null, true, 'membership', '获取当前登录用户的会员等级、有效期与配额信息')}
  1276. ${testSection('membership-benefits', '会员权益', 'GET', 'get', '/api/membership/benefits?level=PLUS', '需认证', 'auth-required',
  1277. [{label:'等级(level)', name:'level', placeholder:'PLUS/PRO/ULTRA', default:'PLUS'}], true, 'membershipBenefit', '查询指定会员等级的权益列表')}
  1278. ${testSection('membership-levels', '等级列表(公开)', 'GET', 'get', '/api/membership/levels', '公开', 'auth-public', null, true, 'levelResponse', '获取所有会员等级列表(PLUS/PRO/ULTRA),含定价方案pricingPlans')}
  1279. ${testSection('membership-level-detail', '等级详情(公开)', 'GET', 'get', '/api/membership/levels/{id}', '公开', 'auth-public',
  1280. [{label:'等级ID(id)', name:'id', placeholder:'1', default:'1'}], true, 'levelResponse', '获取指定等级详情,含定价方案与配额')}`;
  1281. }
  1282. function renderWatchlistModule() {
  1283. return `
  1284. <div class="batch-buttons">
  1285. <button class="btn btn-warning btn-sm" onclick="runWatchlistTests()">运行全部关注列表测试</button>
  1286. </div>
  1287. ${testSection('watchlist-add', '添加关注', 'POST', 'post', '/api/watchlist', '需认证', 'auth-required',
  1288. [{label:'药品名称(drugName)', name:'drugName', placeholder:'阿莫西林胶囊', default:'阿莫西林胶囊'},
  1289. {label:'规格(spec)', name:'spec', placeholder:'0.25g×24片', default:''},
  1290. {label:'厂家(manufacturer)', name:'manufacturer', placeholder:'石药集团', default:''}], true, null, '添加药品到关注列表')}
  1291. ${testSection('watchlist-list', '关注列表', 'GET', 'get', '/api/watchlist', '需认证', 'auth-required', null, true, null, '获取当前用户所有关注的药品列表')}
  1292. ${testSection('watchlist-delete', '取消关注', 'DELETE', 'delete', '/api/watchlist/{id}', '需认证', 'auth-required',
  1293. [{label:'关注ID(id)', name:'id', placeholder:'1', default:'1'}], false, null, '从关注列表中删除')}
  1294. ${testSection('watchlist-stats', '关注统计', 'GET', 'get', '/api/watchlist/stats', '需认证', 'auth-required', null, true, null, '获取关注统计信息')}`;
  1295. }
  1296. function renderTrialQuotaModule() {
  1297. return `
  1298. <div class="batch-buttons">
  1299. <button class="btn btn-warning btn-sm" onclick="runTrialQuotaTests()">运行全部体验配额测试</button>
  1300. </div>
  1301. ${testSection('trial-quota-active', '生效中的体验配额', 'GET', 'get', '/api/trial-quota/active', '公开', 'auth-public', null, true, 'trialQuotaConfig', '查询当前生效的体验配额配置')}`;
  1302. }
  1303. function renderPaymentModule() {
  1304. return `
  1305. <div class="batch-buttons">
  1306. <button class="btn btn-warning btn-sm" onclick="runPaymentTests()">运行全部支付模块测试</button>
  1307. </div>
  1308. ${testSection('payment-plans', '支付方案列表', 'GET', 'get', '/api/payment/plans', '公开', 'auth-public', null, true, null, '获取所有可用的支付方案列表(高级会员月卡/年卡)')}
  1309. ${testSection('payment-create-order', '创建支付订单', 'POST', 'post', '/api/payment/order/create', '需认证', 'auth-required',
  1310. [{label:'方案ID(planId)', name:'planId', placeholder:'1', default:'1'},
  1311. {label:'支付渠道(channel)', name:'channel', placeholder:'WECHAT/ALIPAY', default:'WECHAT'}], true, null, '创建支付订单,微信返回二维码(qrCodeBase64)供扫码支付,支付宝通过alipay.trade.precreate返回qr_code,前端自定义弹窗展示二维码并自动轮询支付状态。15分钟内未付款订单自动过期')}
  1312. ${testSection('payment-query-order', '查询订单', 'GET', 'get', '/api/payment/order/{orderNo}', '需认证', 'auth-required',
  1313. [{label:'订单号(orderNo)', name:'orderNo', placeholder:'OP...', default:''}], true, null, '根据订单号查询支付订单详情,待支付订单会返回当前渠道二维码qrCodeBase64和payUrl')}
  1314. ${testSection('payment-switch-channel', '切换支付渠道', 'POST', 'post', '/api/payment/order/{orderNo}/switch-channel', '需认证', 'auth-required',
  1315. [{label:'订单号(orderNo)', name:'orderNo', placeholder:'OP...', default:''},
  1316. {label:'新渠道(channel)', name:'channel', placeholder:'WECHAT/ALIPAY', default:'ALIPAY'}], true, null, '订单详情页切换支付渠道,不关闭原渠道订单,重新生成新渠道二维码。15分钟内原渠道二维码仍有效,以第一个支付成功渠道为准')}
  1317. ${testSection('payment-orders', '我的订单列表', 'GET', 'get', '/api/payment/orders', '需认证', 'auth-required', null, true, null, '查询当前用户的所有支付订单,待支付订单含expireHint过期提示')}
  1318. ${testSection('payment-mock-pay', '手动确认支付', 'POST', 'post', '/api/payment/order/{orderNo}/pay', '需认证', 'auth-required',
  1319. [{label:'订单号(orderNo)', name:'orderNo', placeholder:'OP...', default:''}], false, null, '开发测试用。mock模式=模拟支付自动激活会员;真实支付模式=手动确认支付(不实际扣款,用于测试回调流程)。需在15分钟过期前操作')}
  1320. ${testSection('payment-callback-wechat', '微信回调', 'POST', 'post', '/api/payment/callback/wechat', '公开', 'auth-public',
  1321. [{label:'订单号(out_trade_no)', name:'out_trade_no', placeholder:'OP...', default:''},
  1322. {label:'交易号(transaction_id,可选)', name:'transaction_id', placeholder:'wx_', default:''}], false, null, '微信支付回调通知。mock模式从参数取订单号,真实支付模式需验签+解密(API v3)')}
  1323. ${testSection('payment-callback-alipay', '支付宝回调', 'POST', 'post', '/api/payment/callback/alipay', '公开', 'auth-public',
  1324. [{label:'订单号(out_trade_no)', name:'out_trade_no', placeholder:'OP...', default:''},
  1325. {label:'交易号(trade_no,可选)', name:'trade_no', placeholder:'2024...', default:''}], false, null, '支付宝回调通知。mock模式从参数取订单号,真实支付模式需RSA2验签')}
  1326. ${testSection('payment-agreement-sign', '开通微信委托代扣(签约)', 'POST', 'post', '/api/payment/agreement/sign', '需认证', 'auth-required',
  1327. [{label:'方案ID(planId)', name:'planId', placeholder:'1', default:'1'},
  1328. {label:'支付渠道(channel)', name:'channel', placeholder:'WECHAT', default:'WECHAT'},
  1329. {label:'周期类型(periodType,可选)', name:'periodType', placeholder:'MONTH/YEAR', default:'MONTH'}], true, 'agreementSignResponse', '微信委托代扣签约。返回签约链接(signUrl)和二维码内容(qrContent),引导用户完成签约。mock模式自动签约成功,10秒后触发首次扣款。仅支持微信渠道(WECHAT),支付宝请使用安心付(小程序会员周期卡)')}
  1330. ${testSection('payment-agreement-list', '我的自动续费协议', 'GET', 'get', '/api/payment/agreement/list', '需认证', 'auth-required', null, true, 'agreementResponse', '查询当前用户所有签约协议列表,含签约状态、下次扣款时间、连续失败次数等')}
  1331. ${testSection('payment-agreement-detail', '协议详情', 'GET', 'get', '/api/payment/agreement/{agreementId}', '需认证', 'auth-required',
  1332. [{label:'协议ID(agreementId)', name:'agreementId', placeholder:'签约后返回', default:''}], true, 'agreementResponse', '查询单个签约协议详情')}
  1333. ${testSection('payment-agreement-unsign', '取消自动续费(解约)', 'POST', 'post', '/api/payment/agreement/{agreementId}/unsign', '需认证', 'auth-required',
  1334. [{label:'协议ID(agreementId)', name:'agreementId', placeholder:'签约后返回', default:''}], false, null, '用户主动取消自动续费。会同步调用第三方解约接口(mock模式直接本地解约),解约后不再自动扣款')}
  1335. ${testSection('payment-agreement-notify-wechat', '微信签约回调通知', 'POST', 'post', '/api/payment/agreement/notify/wechat?out_contract_code=AGxxx&contract_id=MOCK_xxx&change_type=ADD', '公开', 'auth-public',
  1336. [{label:'协议号(out_contract_code)', name:'out_contract_code', placeholder:'签约后返回的externalAgreementNo', default:''},
  1337. {label:'微信协议号(contract_id)', name:'contract_id', placeholder:'微信合同ID', default:'MOCK_AG_test'},
  1338. {label:'变更类型(change_type)', name:'change_type', placeholder:'ADD/DELETE', default:'ADD'}], true, null, '微信委托代扣签约结果异步通知。mock模式返回success。真实模式为加密JSON,需WxPayService解密。ADD=签约成功,DELETE=解约')}
  1339. ${testSection('payment-anxin-notify-deduct', '安心付扣款通知', 'POST', 'post', '/api/payment/anxin/notify/deduct?sub_order_id=AX_SUB_xxx&card_id=MOCK_CARD_xxx&deduct_status=SUCCESS&deduct_amount=9.90&deduct_time=2026-06-30 10:00:00&out_biz_no=1&user_id=2088xxx&order_id=AX_ORDER_xxx', '公开', 'auth-public',
  1340. [{label:'核销子订单号(sub_order_id)', name:'sub_order_id', placeholder:'AX_SUB_...', default:'AX_SUB_TEST001'},
  1341. {label:'卡模板ID(card_id)', name:'card_id', placeholder:'创建卡模板后返回', default:''},
  1342. {label:'扣款状态(deduct_status)', name:'deduct_status', placeholder:'SUCCESS/FAIL', default:'SUCCESS'},
  1343. {label:'扣款金额(deduct_amount)', name:'deduct_amount', placeholder:'9.90', default:'9.90'},
  1344. {label:'扣款时间(deduct_time)', name:'deduct_time', placeholder:'yyyy-MM-dd HH:mm:ss', default:'2026-06-30 10:00:00'},
  1345. {label:'系统用户ID(out_biz_no)', name:'out_biz_no', placeholder:'用户ID', default:'1'},
  1346. {label:'支付宝用户ID(user_id)', name:'user_id', placeholder:'2088...', default:'2088000000000001'},
  1347. {label:'售卖订单ID(order_id)', name:'order_id', placeholder:'AX_ORDER_...', default:'AX_ORDER_TEST001'}], true, null, '安心付周期扣款异步通知。支付宝每次周期扣款后发送,服务端收到后激活/续期会员。mock模式直接处理扣款并激活会员。需先创建卡模板')}
  1348. ${testSection('payment-anxin-notify-card-change', '安心付卡变更通知', 'POST', 'post', '/api/payment/anxin/notify/card-change?card_id=MOCK_CARD_xxx&change_type=AUDIT&status=EFFECTIVE', '公开', 'auth-public',
  1349. [{label:'卡模板ID(card_id)', name:'card_id', placeholder:'创建卡模板后返回', default:''},
  1350. {label:'变更类型(change_type)', name:'change_type', placeholder:'AUDIT/STATUS_CHANGE', default:'AUDIT'},
  1351. {label:'新状态(status)', name:'status', placeholder:'PENDING/EFFECTIVE/INVALID/FROZEN', default:'EFFECTIVE'}], true, null, '安心付卡模板审核结果通知。支付宝在卡模板审核通过/驳回/冻结/解冻时发送。mock模式直接更新本地状态')}`;
  1352. }
  1353. function renderUploadModule() {
  1354. return `
  1355. <div class="batch-buttons">
  1356. <button class="btn btn-warning btn-sm" onclick="runUploadTests()">运行全部上传测试</button>
  1357. </div>
  1358. <div class="test-section">
  1359. <div class="section-title">
  1360. <span class="method method-post">POST</span>
  1361. <span class="path">/api/upload</span>
  1362. <span class="side-tag side-user">用户侧</span>
  1363. <span class="auth-required auth-tag">需认证</span>
  1364. </div>
  1365. <div class="section-desc">上传单个文件,返回文件访问路径。使用 multipart/form-data 格式,字段名: file</div>
  1366. <div class="form-grid">
  1367. <div class="form-group">
  1368. <label>选择文件</label>
  1369. <input type="file" id="upload-file" accept="image/*,.pdf,.doc,.docx" style="padding:8px;">
  1370. </div>
  1371. </div>
  1372. <button class="btn btn-primary" onclick="uploadFile()">上传文件</button>
  1373. <div class="response-box" id="upload-result-box" style="display:none;">
  1374. <div class="response-header">
  1375. <span class="response-title">响应 <span id="upload-duration" style="color:#999;font-weight:400;"></span></span>
  1376. <span class="status-badge" id="upload-status"></span>
  1377. </div>
  1378. <div class="response-content" id="upload-raw"></div>
  1379. </div>
  1380. </div>`;
  1381. }
  1382. function renderSearchModule() {
  1383. return `
  1384. <div class="batch-buttons">
  1385. <button class="btn btn-warning btn-sm" onclick="runSearchTests()">运行全部搜索测试</button>
  1386. </div>
  1387. ${testSection('search-query', '药品搜索', 'POST', 'post', '/api/search/query', '需认证', 'auth-required',
  1388. [{label:'关键词(keyword)', name:'keyword', placeholder:'阿莫西林胶囊', default:'阿莫西林胶囊'},
  1389. {label:'平台列表(platforms,可选)', name:'platforms', placeholder:'yaoshibang,yaobangmang,yiyaocheng', default:'', type:'array'}], true, null, '搜索药品比价,消耗每日配额,返回各平台价格列表。platforms传多个平台代码用逗号分隔,不传则查询全部平台')}
  1390. ${testSection('search-suggestions', '搜索建议', 'GET', 'get', '/api/search/suggestions', '公开', 'auth-public', null, true, null, '获取热门药品搜索建议列表(无需认证)')}`;
  1391. }
  1392. function renderPlatformAccountModule() {
  1393. return `
  1394. <div class="batch-buttons">
  1395. <button class="btn btn-warning btn-sm" onclick="runPlatformAccountTests()">运行全部平台账号测试</button>
  1396. </div>
  1397. ${testSection('pa-bind', '绑定平台账号', 'POST', 'post', '/api/platform-account/bind', '需认证', 'auth-required',
  1398. [{label:'平台代码(platformCode)', name:'platformCode', placeholder:'yaoshibang', default:'yaoshibang'},
  1399. {label:'平台名称(platformName)', name:'platformName', placeholder:'药师帮', default:'药师帮'},
  1400. {label:'账号(account)', name:'account', placeholder:'18776156917', default:'18776156917'},
  1401. {label:'密码(password)', name:'password', placeholder:'your_password', default:'Abc123456'}], false, null, '绑定B2B药品平台账号')}
  1402. ${testSection('pa-list', '已绑定列表', 'GET', 'get', '/api/platform-account/list', '需认证', 'auth-required', null, true, 'platformAccount', '查询已绑定的所有平台账号')}
  1403. ${testSection('pa-verify', '验证账号', 'POST', 'post', '/api/platform-account/verify/{platformCode}', '需认证', 'auth-required',
  1404. [{label:'平台代码(platformCode)', name:'platformCode', placeholder:'yaoshibang', default:'yaoshibang'}], false, null, '验证已绑定的平台账号是否有效')}
  1405. ${testSection('pa-unbind', '解绑账号', 'DELETE', 'delete', '/api/platform-account/unbind/{platformCode}', '需认证', 'auth-required',
  1406. [{label:'平台代码(platformCode)', name:'platformCode', placeholder:'yaoshibang', default:'yaoshibang'}], false, null, '解绑指定平台账号')}`;
  1407. }
  1408. function renderBusinessLicenseModule() {
  1409. return `
  1410. <div class="batch-buttons">
  1411. <button class="btn btn-warning btn-sm" onclick="runBusinessLicenseTests()">运行全部营业执照测试</button>
  1412. </div>
  1413. ${testSection('bl-upload', '上传执照(JSON)', 'POST', 'post', '/api/business-license/upload', '需认证', 'auth-required',
  1414. [{label:'图片URL(licenseImageUrl)', name:'licenseImageUrl', placeholder:'https://oss.example.com/license/xxx.jpg', default:'https://oss.example.com/license/test.jpg'},
  1415. {label:'信用代码(creditCode)', name:'creditCode', placeholder:'91110000XXXXXXXXXX', default:'91110000TEST000001'},
  1416. {label:'药店名称(pharmacyName)', name:'pharmacyName', placeholder:'XX大药房', default:'测试大药房'},
  1417. {label:'法人(legalPerson,可选)', name:'legalPerson', placeholder:'张三', default:'张三'}], false, null, '通过JSON方式上传营业执照(传图片URL)')}
  1418. ${testSection('bl-info', '查询执照信息', 'GET', 'get', '/api/business-license/info', '需认证', 'auth-required', null, true, 'businessLicense', '查询当前用户已上传的营业执照信息')}`;
  1419. }
  1420. function renderPlatformConfigModule() {
  1421. return `
  1422. <div class="batch-buttons">
  1423. <button class="btn btn-warning btn-sm" onclick="runPlatformConfigTests()">运行全部平台配置测试</button>
  1424. </div>
  1425. ${testSection('pc-enabled', '启用平台列表', 'GET', 'get', '/api/platform-config/enabled', '公开', 'auth-public', null, true, 'platformConfig', '获取所有已启用的爬虫平台配置列表')}
  1426. ${testSection('pc-detail', '平台详情', 'GET', 'get', '/api/platform-config/{platformCode}', '公开', 'auth-public',
  1427. [{label:'平台代码(platformCode)', name:'platformCode', placeholder:'yaoshibang', default:'yaoshibang'}], true, 'platformConfig', '获取指定平台的详细配置信息')}`;
  1428. }
  1429. function renderSearchRecordModule() {
  1430. return `
  1431. <div class="batch-buttons">
  1432. <button class="btn btn-warning btn-sm" onclick="runSearchRecordTests()">运行全部搜索记录测试</button>
  1433. </div>
  1434. ${testSection('sr-save', '保存搜索记录', 'POST', 'post', '/api/search-record/history', '需认证', 'auth-required',
  1435. [{label:'药品名称(drugName)', name:'drugName', placeholder:'阿莫西林', default:'阿莫西林'},
  1436. {label:'搜索时间(searchedAt)', name:'searchedAt', placeholder:'yyyy-MM-dd HH:mm:ss', default:'2026-06-26 08:30:00'},
  1437. {label:'状态文案(statusText)', name:'statusText', placeholder:'已返回 2 个平台的可比价结果。', default:'已返回 2 个平台的可比价结果。'},
  1438. {label:'平台结果(platformResults)', name:'platformResults', placeholder:'[{"platformId":"yiyaocheng","platformName":"壹药城"}]', default:'[{"platformId":"yiyaocheng","platformName":"壹药城","response":{"status":"ok","products":[{"drugName":"阿莫西林","price":12.5}]}}]', type:'json'},
  1439. {label:'搜索日志(searchLog)', name:'searchLog', placeholder:'null', default:'null', type:'json'}], true, 'searchRecordResponse', '保存一次完整的比价搜索快照。platformResults为JSON数组,每个元素含platformId/platformName/response;后端会自动计算platformCount(平台数)和quoteCount(报价条数)。searchedAt格式:yyyy-MM-dd HH:mm:ss')}
  1440. ${testSection('sr-list', '搜索记录列表', 'GET', 'get', '/api/search-record/history?pageNo={pageNo}&pageSize={pageSize}', '需认证', 'auth-required',
  1441. [{label:'页码(pageNo)', name:'pageNo', placeholder:'1', default:'1'},
  1442. {label:'每页条数(pageSize)', name:'pageSize', placeholder:'20', default:'20'}], true, 'searchRecordResponse', '分页查询当前用户搜索记录(轻量摘要,不含platformResults/searchLog大字段),返回分页对象含total/records/pages等字段')}
  1443. ${testSection('sr-detail', '记录详情', 'GET', 'get', '/api/search-record/history/{id}', '需认证', 'auth-required',
  1444. [{label:'记录ID(id)', name:'id', placeholder:'1', default:'1'}], true, 'searchRecordResponse', '读取单条搜索记录详情(含完整platformResults和searchLog)')}
  1445. ${testSection('sr-delete', '删除记录', 'DELETE', 'delete', '/api/search-record/history/{id}', '需认证', 'auth-required',
  1446. [{label:'记录ID(id)', name:'id', placeholder:'1', default:'1'}], false, null, '删除单条搜索记录(幂等,不存在也返回成功)')}
  1447. ${testSection('sr-clear', '清空记录', 'DELETE', 'delete', '/api/search-record/history', '需认证', 'auth-required', null, false, null, '清空当前用户全部搜索记录')}`;
  1448. }
  1449. function renderPlatformSessionModule() {
  1450. return `
  1451. <div class="batch-buttons">
  1452. <button class="btn btn-warning btn-sm" onclick="runPlatformSessionTests()">运行全部平台会话测试</button>
  1453. </div>
  1454. ${testSection('ps-upload', '上传平台会话', 'PUT', 'put', '/api/platform-session/{platformCode}', '需认证', 'auth-required',
  1455. [{label:'平台代码(platformCode)', name:'platformCode', placeholder:'yiyaocheng', default:'yiyaocheng'},
  1456. {label:'平台名称(platformName)', name:'platformName', placeholder:'壹药城', default:'壹药城'},
  1457. {label:'账号标签(accountLabel)', name:'accountLabel', placeholder:'深圳大方无限科技有限公司', default:'测试账号'},
  1458. {label:'登录URL(loginUrl)', name:'loginUrl', placeholder:'https://mall.yaoex.com/login', default:'https://mall.yaoex.com/login'},
  1459. {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必须与路径中的一致')}
  1460. ${testSection('ps-list', '平台会话列表', 'GET', 'get', '/api/platform-session/list', '需认证', 'auth-required', null, true, null, '获取当前用户所有已保存的平台会话(Cookie已解密),用于启动时恢复登录态')}
  1461. ${testSection('ps-delete', '删除平台会话', 'DELETE', 'delete', '/api/platform-session/{platformCode}', '需认证', 'auth-required',
  1462. [{label:'平台代码(platformCode)', name:'platformCode', placeholder:'yiyaocheng', default:'yiyaocheng'}], false, null, '删除某个平台的远端会话(幂等,不存在也返回成功)')}`;
  1463. }
  1464. // ==================== 运营侧模块 ====================
  1465. function renderAdminMembershipModule() {
  1466. return `
  1467. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminMembershipTests()">运行全部会员管理测试</button></div>
  1468. ${testSection('admin-membership-grant', '发放/调整会员', 'POST', 'post', '/api/admin/membership/grant', '需认证', 'auth-required',
  1469. [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'},
  1470. {label:'会员等级(level)', name:'level', placeholder:'PLUS/PRO/ULTRA', default:'PRO'},
  1471. {label:'来源(source)', name:'source', placeholder:'ADMIN_GRANT', default:'ADMIN_GRANT'},
  1472. {label:'备注(remark)', name:'remark', placeholder:'运营发放', default:'测试发放'}], false, 'membership', '运营/超管给用户发放或调整会员等级')}
  1473. ${testSection('admin-membership-list', '会员记录列表', 'GET', 'get', '/api/admin/membership/memberships?page=1&size=10', '需认证', 'auth-required', null, true, 'membership', '分页查询会员发放记录')}`;
  1474. }
  1475. function renderAdminInviteModule() {
  1476. return `
  1477. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminInviteTests()">运行全部邀请管理测试</button></div>
  1478. ${testSection('admin-invite-config', '邀请配置查询', 'GET', 'get', '/api/admin/invite/config', '需认证', 'auth-required', null, true, 'inviteConfig', '获取当前激活的邀请配置')}
  1479. ${testSection('admin-invite-configs', '全部邀请配置', 'GET', 'get', '/api/admin/invite/configs', '需认证', 'auth-required', null, true, 'inviteConfig', '获取所有邀请配置历史记录')}
  1480. ${testSection('admin-invite-config-update', '更新邀请配置', 'PUT', 'put', '/api/admin/invite/config', '需认证', 'auth-required',
  1481. [
  1482. {label:'奖励月数(rewardMonths)', name:'rewardMonths', placeholder:'1', default:'1'},
  1483. {label:'总邀请上限(maxTotalInvites)', name:'maxTotalInvites', placeholder:'30', default:'30'},
  1484. {label:'日奖励上限(maxDailyReward)', name:'maxDailyReward', placeholder:'50', default:'50'},
  1485. {label:'邀请码过期天(inviteCodeExpireDays)', name:'inviteCodeExpireDays', placeholder:'30', default:'30'},
  1486. {label:'日邀请上限(maxInvitePerDay)', name:'maxInvitePerDay', placeholder:'10', default:'10'},
  1487. {label:'APP下载URL(appDownloadUrl)', name:'appDownloadUrl', placeholder:'https://...', default:'https://download.example.com/app.exe'},
  1488. {label:'小程序路径(miniappPath)', name:'miniappPath', placeholder:'/pages/download/index', default:''},
  1489. {label:'小程序AppID(miniappAppId)', name:'miniappAppId', placeholder:'wx...', default:''},
  1490. {label:'微信跳转URL(wechatRedirectUrl)', name:'wechatRedirectUrl', placeholder:'https://...', default:''},
  1491. {label:'钉钉AppID(dingtalkAppId)', name:'dingtalkAppId', placeholder:'', default:''},
  1492. {label:'飞书AppID(feishuAppId)', name:'feishuAppId', placeholder:'', default:''},
  1493. {label:'落地页标题(landingTitle)', name:'landingTitle', placeholder:'邀请你加入智价云(药店版)', default:'邀请你加入智价云(药店版)'},
  1494. {label:'落地页描述(landingDesc)', name:'landingDesc', placeholder:'登录智价云(药店版),聚合比价查低价', default:''},
  1495. {label:'应用名称(appName)', name:'appName', placeholder:'智价云(药店版)', default:'智价云(药店版)'},
  1496. {label:'分享标题模板(shareTitleTemplate)', name:'shareTitleTemplate', placeholder:'{inviter} 邀请你加入{appName}', default:'{inviter} 邀请你加入{appName}'},
  1497. {label:'复制文案模板(copyTextTemplate)', name:'copyTextTemplate', placeholder:'我是{inviter},在这里发现了一个药店采购神器...', default:'我是{inviter},在这里发现了一个药店采购神器——{appName}!它聚合比价功能特别方便,能快速查到最低价,帮你节省采购成本。下载链接{inviteLink}填我的邀请码 {code} 完成注册,你也会获得会员权益!'},
  1498. {label:'奖励说明(rewardDescription)', name:'rewardDescription', placeholder:'邀请人每成功邀请1家药店注册...', default:'邀请人每成功邀请1家药店注册,赠送1个月高级会员时长'},
  1499. {label:'统计摘要模板(summaryTextTemplate)', name:'summaryTextTemplate', placeholder:'已邀请{totalInvited}家药店...', default:'已邀请{totalInvited}家药店,{registeredCount}家注册,累计获得{totalReward}个月高级会员'},
  1500. {label:'Windows提示文案(instructionWindows)', name:'instructionWindows', placeholder:'下载安装Windows客户端后...', default:'下载安装Windows客户端后,在注册页面输入邀请码:{code} 或点击链接自动填入'},
  1501. {label:'App提示文案(instructionApp)', name:'instructionApp', placeholder:'下载安装客户端后...', default:'下载安装客户端后,在注册页面输入邀请码:{code} 或点击链接自动填入'},
  1502. {label:'小程序提示文案(instructionMiniapp)', name:'instructionMiniapp', placeholder:'点击按钮打开小程序...', default:'点击按钮打开小程序,邀请码已自动填入'},
  1503. {label:'微信提示文案(instructionWechat)', name:'instructionWechat', placeholder:'关注公众号后...', default:'关注公众号后,在菜单中输入邀请码:{code} 完成注册'},
  1504. {label:'钉钉提示文案(instructionDingtalk)', name:'instructionDingtalk', placeholder:'打开钉钉应用...', default:'打开钉钉应用,搜索"{appName}"并输入邀请码:{code}'},
  1505. {label:'飞书提示文案(instructionFeishu)', name:'instructionFeishu', placeholder:'打开飞书应用...', default:'打开飞书应用,搜索"{appName}"并输入邀请码:{code}'},
  1506. {label:'默认提示文案(instructionDefault)', name:'instructionDefault', placeholder:'下载客户端后...', default:'下载客户端后,在注册页面输入邀请码:{code} 即可享受奖励'},
  1507. {label:'状态(status)', name:'status', placeholder:'0停用/1启用', default:'1'}
  1508. ], false, 'inviteConfig', '更新邀请配置参数。文案模板支持占位符:{inviter}邀请人昵称、{appName}应用名称、{inviteLink}邀请链接、{code}邀请码、{rewardMonths}奖励月数、{totalInvited}总邀请数、{registeredCount}注册数、{totalReward}总奖励')}
  1509. ${testSection('admin-invite-rewards', '邀请记录查询', 'GET', 'get', '/api/admin/invite/rewards?userId=1', '需认证', 'auth-required', null, true, 'inviteReward', '查看指定用户的邀请奖励记录')}
  1510. ${testSection('admin-invite-conversion', '邀请转化统计', 'GET', 'get', '/api/admin/invite/conversion-stats?userId=1', '需认证', 'auth-required', null, true, 'conversionStats', '查看用户邀请点击-注册转化率')}`;
  1511. }
  1512. function renderAdminCrawlerModule() {
  1513. return `
  1514. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminCrawlerTests()">运行全部爬虫管理测试</button></div>
  1515. ${testSection('admin-crawler-grant', '手动发放配额', 'POST', 'post', '/api/admin/crawler/grant', '需认证', 'auth-required',
  1516. [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'},
  1517. {label:'配额数(quotaCount)', name:'quotaCount', placeholder:'50', default:'10'},
  1518. {label:'过期天数(expireDays)', name:'expireDays', placeholder:'30', default:'30'},
  1519. {label:'备注(remark)', name:'remark', placeholder:'VIP升级奖励', default:'测试发放'}], false, 'quotaGrant', '运营手动给用户发放爬虫次数')}
  1520. ${testSection('admin-crawler-grants', '发放记录', 'GET', 'get', '/api/admin/crawler/grants', '需认证', 'auth-required', null, true, 'quotaGrant', '查询爬虫配额发放记录')}
  1521. ${testSection('admin-crawler-user-detail', '用户爬虫详情', 'GET', 'get', '/api/admin/crawler/user-detail?userId=1', '需认证', 'auth-required',
  1522. [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'}], true, 'crawlerUserDetail', '查看用户爬虫使用详情及各平台分布')}`;
  1523. }
  1524. function renderAdminCouponModule() {
  1525. return `
  1526. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminCouponTests()">运行全部优惠券发放测试</button></div>
  1527. ${testSection('admin-coupon-issue', '发放优惠券', 'POST', 'post', '/api/coupon/issue', '需认证', 'auth-required',
  1528. [{label:'模板ID(couponId)', name:'couponId', placeholder:'1', default:'1'},
  1529. {label:'用户ID列表(userIds)', name:'userIds', placeholder:'1,2,3', default:'1', type:'array'}], false, 'couponIssue', '管理员向指定用户发放优惠券,userIds统一用列表传入,单个用户填1即可')}`;
  1530. }
  1531. function renderAdminLicenseModule() {
  1532. return `
  1533. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminLicenseTests()">运行全部执照审核测试</button></div>
  1534. ${testSection('admin-license-list', '审核列表', 'GET', 'get', '/api/admin/license/review?reviewStatus=PENDING', '需认证', 'auth-required', null, true, 'businessLicense', '查询待审核/已审核的营业执照列表')}
  1535. ${testSection('admin-license-review', '审核执照', 'POST', 'post', '/api/admin/license/review', '需认证', 'auth-required',
  1536. [{label:'执照ID(licenseId)', name:'licenseId', placeholder:'1', default:'1'},
  1537. {label:'动作(action)', name:'action', placeholder:'APPROVED/REJECTED', default:'APPROVED'},
  1538. {label:'驳回原因(rejectReason)', name:'rejectReason', placeholder:'驳回时填写', default:''}], false, null, '审核营业执照,通过或驳回')}`;
  1539. }
  1540. function renderAdminPaymentModule() {
  1541. return `
  1542. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminPaymentTests()">运行全部支付方案测试</button></div>
  1543. ${testSection('admin-payment-list', '方案列表(含下架)', 'GET', 'get', '/api/admin/payment/plans', '需认证', 'auth-required', null, true, null, '查询所有支付方案(含已下架),管理端专用')}
  1544. ${testSection('admin-payment-create', '创建方案', 'POST', 'post', '/api/admin/payment/plans', '需认证', 'auth-required',
  1545. [{label:'方案编码(planCode)', name:'planCode', placeholder:'MONTHLY_PLUS', default:'MONTHLY_PLUS'},
  1546. {label:'方案名称(planName)', name:'planName', placeholder:'普通会员·月卡', default:'普通会员·月卡'},
  1547. {label:'价格(price)', name:'price', placeholder:'9.90', default:'9.90'},
  1548. {label:'有效天数(durationDays)', name:'durationDays', placeholder:'30', default:'30'},
  1549. {label:'会员等级(membershipLevel)', name:'membershipLevel', placeholder:'PLUS', default:'PLUS'}], false, null, '管理员创建新的支付方案')}
  1550. ${testSection('admin-payment-update', '更新方案', 'PUT', 'put', '/api/admin/payment/plans/{id}', '需认证', 'auth-required',
  1551. [{label:'方案ID(id)', name:'id', placeholder:'1', default:'1'},
  1552. {label:'方案编码(planCode)', name:'planCode', placeholder:'MONTHLY_PLUS', default:'MONTHLY_PLUS'},
  1553. {label:'方案名称(planName)', name:'planName', placeholder:'普通会员·月卡', default:'普通会员·月卡(更新)'},
  1554. {label:'价格(price)', name:'price', placeholder:'9.90', default:'9.90'},
  1555. {label:'有效天数(durationDays)', name:'durationDays', placeholder:'30', default:'30'},
  1556. {label:'会员等级(membershipLevel)', name:'membershipLevel', placeholder:'PLUS', default:'PLUS'}], false, null, '管理员更新支付方案')}
  1557. ${testSection('admin-payment-toggle', '切换方案状态', 'PUT', 'put', '/api/admin/payment/plans/{id}/status', '需认证', 'auth-required',
  1558. [{label:'方案ID(id)', name:'id', placeholder:'1', default:'1'},
  1559. {label:'状态(status)', name:'status', placeholder:'0下架/1上架', default:'0'}], false, null, '切换支付方案上架/下架状态')}`;
  1560. }
  1561. function renderAdminAnxinModule() {
  1562. return `
  1563. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminAnxinTests()">运行全部安心付管理测试</button></div>
  1564. ${testSection('admin-anxin-create', '创建安心付卡模板', 'POST', 'post', '/api/admin/payment/anxin/card/create', '需认证', 'auth-required',
  1565. [{label:'支付方案ID(planId)', name:'planId', placeholder:'1', default:'1'}], true, 'anxinCard', '为支付方案创建支付宝安心付周期卡模板。创建后状态为PENDING(待审核),支付宝审核通过后变为EFFECTIVE。mock模式直接返回EFFECTIVE状态。每个支付方案只能创建一次')}
  1566. ${testSection('admin-anxin-list', '卡模板列表', 'GET', 'get', '/api/admin/payment/anxin/cards', '需认证', 'auth-required', null, true, 'anxinCard', '查询所有安心付卡模板列表,含卡模板ID、状态、周期金额等')}
  1567. ${testSection('admin-anxin-sync', '同步卡模板状态', 'POST', 'post', '/api/admin/payment/anxin/card/{cardId}/sync', '需认证', 'auth-required',
  1568. [{label:'卡模板ID(cardId)', name:'cardId', placeholder:'创建后返回', default:''}], true, 'anxinCard', '调用支付宝接口查询卡模板最新状态(审核结果等)。mock模式直接返回本地状态')}
  1569. ${testSection('admin-anxin-status', '修改卡模板状态', 'PUT', 'put', '/api/admin/payment/anxin/card/{cardId}/status', '需认证', 'auth-required',
  1570. [{label:'卡模板ID(cardId)', name:'cardId', placeholder:'创建后返回', default:''},
  1571. {label:'状态(status)', name:'status', placeholder:'EFFECTIVE/INVALID/FROZEN', default:'EFFECTIVE'}], false, null, '修改卡模板状态:EFFECTIVE(上架)/ INVALID(下架)/ FROZEN(冻结)')}`;
  1572. }
  1573. function renderAdminTrialQuotaModule() {
  1574. return `
  1575. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminTrialQuotaTests()">运行全部体验配额测试</button></div>
  1576. ${testSection('admin-tq-active', '当前体验配置', 'GET', 'get', '/api/trial-quota/active', '公开', 'auth-public', null, true, 'trialQuota', '获取当前激活的体验配额配置')}
  1577. ${testSection('admin-tq-list', '所有配置', 'GET', 'get', '/api/admin/trial-quota/configs', '需认证', 'auth-required', null, true, 'trialQuota', '查询所有体验配额配置列表')}
  1578. ${testSection('admin-tq-create', '创建配置', 'POST', 'post', '/api/admin/trial-quota/config', '需认证', 'auth-required',
  1579. [{label:'配置名称(configName)', name:'configName', placeholder:'新用户默认体验', default:'测试体验配置'},
  1580. {label:'体验天数(trialDays)', name:'trialDays', placeholder:'15', default:'15'},
  1581. {label:'日查询限制(dailyQueryLimit)', name:'dailyQueryLimit', placeholder:'20', default:'20'},
  1582. {label:'月关注限制(monthlyWatchlistLimit)', name:'monthlyWatchlistLimit', placeholder:'199', default:'199'},
  1583. {label:'配置类型(configType)', name:'configType', placeholder:'DEFAULT', default:'DEFAULT'},
  1584. {label:'用户类型(applicableUserType)', name:'applicableUserType', placeholder:'NEW_USER', default:'NEW_USER'}], false, 'trialQuota', '创建新的体验配额配置')}
  1585. ${testSection('admin-tq-update', '更新配置', 'PUT', 'put', '/api/admin/trial-quota/config/{id}', '需认证', 'auth-required',
  1586. [{label:'配置ID(id)', name:'id', placeholder:'1', default:'1'},
  1587. {label:'配置名称(configName)', name:'configName', placeholder:'更新后名称', default:'更新后的体验配置'},
  1588. {label:'体验天数(trialDays)', name:'trialDays', placeholder:'30', default:'30'},
  1589. {label:'日查询限制(dailyQueryLimit)', name:'dailyQueryLimit', placeholder:'50', default:'50'},
  1590. {label:'月关注限制(monthlyWatchlistLimit)', name:'monthlyWatchlistLimit', placeholder:'199', default:'199'}], false, 'trialQuota', '更新指定体验配额配置')}
  1591. ${testSection('admin-tq-delete', '删除配置', 'DELETE', 'delete', '/api/admin/trial-quota/config/{id}', '需认证', 'auth-required',
  1592. [{label:'配置ID(id)', name:'id', placeholder:'1', default:'1'}], false, null, '删除指定体验配额配置')}`;
  1593. }
  1594. function renderAdminUserModule() {
  1595. return `
  1596. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminUserTests()">运行全部用户管理测试</button></div>
  1597. ${testSection('admin-user-list', '前端用户列表', 'GET', 'get', '/api/admin/users?page={page}&size={size}&phone={phone}&nickname={nickname}', '需认证', 'auth-required',
  1598. [{label:'页码(page)', name:'page', placeholder:'1', default:'1'},
  1599. {label:'每页条数(size)', name:'size', placeholder:'10', default:'10'},
  1600. {label:'手机号(phone)', name:'phone', placeholder:'138', default:''},
  1601. {label:'昵称(nickname)', name:'nickname', placeholder:'测试', default:''}], true, null, '查询前端注册用户列表(仅USER角色)')}
  1602. ${testSection('admin-admins-list', '管理员列表', 'GET', 'get', '/api/admin/admins?page=1&size=10', '需认证', 'auth-required', null, true, null, '查询管理员列表(t_admin表)')}
  1603. ${testSection('admin-user-role', '修改用户角色', 'PUT', 'put', '/api/admin/users/{userId}/role', '需认证', 'auth-required',
  1604. [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'},
  1605. {label:'目标角色(role)', name:'role', placeholder:'ADMIN/SUPER_ADMIN/USER', default:'ADMIN'}], false, null, '升级用户为管理员(写入t_admin)/ 降级管理员为用户(删除t_admin记录)')}
  1606. ${testSection('admin-user-kick', '强制踢出用户', 'POST', 'post', '/api/admin/users/{userId}/kick', '需认证', 'auth-required',
  1607. [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'}], false, null, '强制踢出前端用户:递增tokenVersion+清空RefreshToken')}
  1608. ${testSection('admin-admin-kick', '强制踢出管理员', 'POST', 'post', '/api/admin/admins/{adminId}/kick', '需认证', 'auth-required',
  1609. [{label:'管理员ID(adminId)', name:'adminId', placeholder:'100', default:'100'}], false, null, '强制踢出管理员:递增tokenVersion+清空RefreshToken')}`;
  1610. }
  1611. function renderAdminAuditModule() {
  1612. return `
  1613. <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminAuditTests()">运行全部审计日志测试</button></div>
  1614. ${testSection('admin-audit-query', '查询审计日志', 'GET', 'get', '/api/admin/audit-log?page=1&size=10', '需认证', 'auth-required', null, true, 'auditLog', '分页查询操作审计日志')}
  1615. ${testSection('admin-audit-sensitive', '敏感操作日志', 'GET', 'get', '/api/admin/audit-log/sensitive', '需认证', 'auth-required', null, true, 'auditLog', '查询所有敏感操作日志')}
  1616. ${testSection('admin-audit-user', '用户操作日志', 'GET', 'get', '/api/admin/audit-log/user/{userId}?limit=20', '需认证', 'auth-required',
  1617. [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'}], true, 'auditLog', '查询指定用户的操作日志')}
  1618. ${testSection('admin-audit-target', '目标操作历史', 'GET', 'get', '/api/admin/audit-log/target/{targetType}/{targetId}?limit=20', '需认证', 'auth-required',
  1619. [{label:'目标类型(targetType)', name:'targetType', placeholder:'USER', default:'USER'},
  1620. {label:'目标ID(targetId)', name:'targetId', placeholder:'1', default:'1'}], true, 'auditLog', '查询某个目标对象的操作历史')}
  1621. ${testSection('admin-audit-stats', '模块操作统计', 'GET', 'get', '/api/admin/audit-log/stats?module=USER', '需认证', 'auth-required', null, true, null, '统计指定模块的操作次数')}`;
  1622. }
  1623. // ===================== 批量测试 =====================
  1624. function getSectionsInModule(modulePrefix) {
  1625. return Array.from(document.querySelectorAll('.test-section')).filter(s => {
  1626. const btn = s.querySelector('button[onclick*="runTest"]');
  1627. return btn && btn.getAttribute('onclick').includes("'" + modulePrefix + "-");
  1628. });
  1629. }
  1630. const USER_MODULES = ['auth','crawler','invite','coupon','membership','payment','upload','search','platform-account','business-license','platform-config','search-record','platform-session','watchlist','trial-quota'];
  1631. const ADMIN_MODULES = ['admin-membership','admin-invite','admin-crawler','admin-coupon','admin-license','admin-payment','admin-anxin','admin-trial-quota','admin-user','admin-audit','upload'];
  1632. function countTestsInCurrentPanel() {
  1633. return document.querySelectorAll('.test-section button[onclick*="runTest"]').length;
  1634. }
  1635. async function runBatchTests(modulePrefix) {
  1636. const sections = getSectionsInModule(modulePrefix);
  1637. // 将该模块中尚未运行过的测试计入total
  1638. for (const section of sections) {
  1639. const btn = section.querySelector('button[onclick*="runTest"]');
  1640. if (btn) {
  1641. const onclick = btn.getAttribute('onclick') || '';
  1642. const match = onclick.match(/runTest\('([^']+)'/);
  1643. if (match && !stats.hasOwnProperty(match[1])) {
  1644. stats.total++;
  1645. }
  1646. }
  1647. }
  1648. stats.pending = Math.max(0, stats.total - stats.pass - stats.fail);
  1649. document.getElementById('totalCount').textContent = stats.total;
  1650. document.getElementById('pendingCount').textContent = stats.pending;
  1651. for (const section of sections) {
  1652. const btn = section.querySelector('button[onclick*="runTest"]');
  1653. if (btn) {
  1654. btn.click();
  1655. await new Promise(r => setTimeout(r, 600));
  1656. }
  1657. }
  1658. }
  1659. async function runAllTests() {
  1660. resetStats();
  1661. // 先遍历用户侧模块
  1662. for (const m of USER_MODULES) {
  1663. const tab = document.querySelector(`#moduleTabs .module-tab[data-module="${m}"]`);
  1664. if (tab) tab.click();
  1665. await new Promise(r => setTimeout(r, 50));
  1666. stats.total += countTestsInCurrentPanel();
  1667. }
  1668. // 再遍历运营侧模块
  1669. for (const m of ADMIN_MODULES) {
  1670. const tab = document.querySelector(`#adminModuleTabs .module-tab[data-module="${m}"]`);
  1671. if (tab) tab.click();
  1672. await new Promise(r => setTimeout(r, 50));
  1673. stats.total += countTestsInCurrentPanel();
  1674. }
  1675. document.getElementById('totalCount').textContent = stats.total;
  1676. stats.pending = stats.total;
  1677. document.getElementById('pendingCount').textContent = stats.pending;
  1678. // 逐模块执行 - 用户侧
  1679. switchSide('user');
  1680. for (const m of USER_MODULES) {
  1681. try {
  1682. const tab = document.querySelector(`#moduleTabs .module-tab[data-module="${m}"]`);
  1683. if (tab) tab.click();
  1684. await new Promise(r => setTimeout(r, 100));
  1685. await runBatchTests(m);
  1686. } catch (e) { console.error('模块 ' + m + ' 测试出错:', e); }
  1687. }
  1688. // 逐模块执行 - 运营侧
  1689. switchSide('admin');
  1690. for (const m of ADMIN_MODULES) {
  1691. try {
  1692. const tab = document.querySelector(`#adminModuleTabs .module-tab[data-module="${m}"]`);
  1693. if (tab) tab.click();
  1694. await new Promise(r => setTimeout(r, 100));
  1695. await runBatchTests(m);
  1696. } catch (e) { console.error('模块 ' + m + ' 测试出错:', e); }
  1697. }
  1698. }
  1699. function resetStats() {
  1700. const keys = Object.keys(stats);
  1701. for (const k of keys) {
  1702. if (k !== 'pass' && k !== 'fail' && k !== 'pending' && k !== 'total') {
  1703. delete stats[k];
  1704. }
  1705. }
  1706. stats.pass = 0;
  1707. stats.fail = 0;
  1708. stats.pending = 0;
  1709. stats.total = 0;
  1710. document.getElementById('totalCount').textContent = '0';
  1711. document.getElementById('passCount').textContent = '0';
  1712. document.getElementById('failCount').textContent = '0';
  1713. document.getElementById('pendingCount').textContent = '0';
  1714. }
  1715. function clearAllResults() {
  1716. stopAlipayPolling();
  1717. closeAlipayModal();
  1718. document.querySelectorAll('.response-box').forEach(b => b.style.display = 'none');
  1719. document.querySelectorAll('.status-badge').forEach(b => { b.textContent = ''; b.className = 'status-badge'; });
  1720. resetStats();
  1721. }
  1722. // ===================== 支付宝支付弹窗 =====================
  1723. let currentAlipayModal = null;
  1724. let currentAlipayOrderNo = null;
  1725. function showAlipayModal(qrBase64, amount, orderNo) {
  1726. closeAlipayModal();
  1727. currentAlipayOrderNo = orderNo;
  1728. const overlay = document.createElement('div');
  1729. overlay.className = 'alipay-modal-overlay';
  1730. overlay.id = 'alipay-modal-overlay';
  1731. overlay.onclick = (e) => { if (e.target === overlay) closeAlipayModal(); };
  1732. const amountText = amount ? '<span>¥</span>' + amount : '';
  1733. overlay.innerHTML = '<div class="alipay-modal">'
  1734. + '<button class="alipay-modal-close" onclick="closeAlipayModal()" title="关闭">×</button>'
  1735. + '<div class="alipay-modal-logo">'
  1736. + '<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>'
  1737. + '支付宝'
  1738. + '</div>'
  1739. + '<div class="alipay-modal-title">扫一扫付款</div>'
  1740. + '<div class="alipay-modal-amount">' + amountText + '</div>'
  1741. + '<div class="alipay-modal-qr"><img src="' + qrBase64 + '" alt="支付宝付款码"></div>'
  1742. + '<div class="alipay-modal-status" id="alipay-modal-status">轮询支付状态中...</div>'
  1743. + '<div class="alipay-modal-hint">二维码有效期 15 分钟<br>请尽快完成付款<br>支付成功后回调自动激活,也可手动点击“确认支付”测试回调</div>'
  1744. + '</div>';
  1745. document.body.appendChild(overlay);
  1746. currentAlipayModal = overlay;
  1747. if (orderNo) startAlipayPolling(orderNo);
  1748. }
  1749. function closeAlipayModal() {
  1750. if (currentAlipayModal) {
  1751. currentAlipayModal.remove();
  1752. currentAlipayModal = null;
  1753. currentAlipayOrderNo = null;
  1754. }
  1755. }
  1756. // ===================== 支付宝支付轮询 =====================
  1757. let alipayPollTimer = null;
  1758. function startAlipayPolling(orderNo) {
  1759. stopAlipayPolling();
  1760. const statusEl = document.getElementById('alipay-modal-status');
  1761. if (statusEl) statusEl.textContent = '轮询支付状态中...';
  1762. alipayPollTimer = setInterval(async () => {
  1763. try {
  1764. const res = await api('GET', '/api/payment/order/' + orderNo, null, true);
  1765. const status = res.data?.data?.status;
  1766. const el = document.getElementById('alipay-modal-status');
  1767. if (!el || !currentAlipayModal) { stopAlipayPolling(); return; }
  1768. if (status === 'PAID') {
  1769. stopAlipayPolling();
  1770. el.innerHTML = '<span style="color:#2e7d32;font-weight:600;">✓ 支付成功!订单已激活</span>';
  1771. } else if (status === 'EXPIRED') {
  1772. stopAlipayPolling();
  1773. el.innerHTML = '<span style="color:#c62828;">订单已过期</span>';
  1774. } else {
  1775. el.textContent = '轮询支付状态中... (当前: ' + (status || '未知') + ')';
  1776. }
  1777. } catch (e) {
  1778. console.error('轮询支付状态异常:', e);
  1779. }
  1780. }, 3000);
  1781. }
  1782. function stopAlipayPolling() {
  1783. if (alipayPollTimer) { clearInterval(alipayPollTimer); alipayPollTimer = null; }
  1784. }
  1785. // 用户侧快捷批量运行
  1786. function runAuthTests() { runBatchTests('auth'); }
  1787. function runCrawlerTests() { runBatchTests('crawler'); }
  1788. function runInviteTests() { runBatchTests('invite'); }
  1789. function runCouponTests() { runBatchTests('coupon'); }
  1790. function runMembershipTests() { runBatchTests('membership'); }
  1791. function runPaymentTests() { runBatchTests('payment'); }
  1792. function runUploadTests() { uploadFile(); }
  1793. function runSearchTests() { runBatchTests('search'); }
  1794. function runPlatformAccountTests() { runBatchTests('pa'); }
  1795. function runBusinessLicenseTests() { runBatchTests('bl'); }
  1796. function runPlatformConfigTests() { runBatchTests('pc'); }
  1797. function runSearchRecordTests() { runBatchTests('sr'); }
  1798. function runPlatformSessionTests() { runBatchTests('ps'); }function runWatchlistTests() { runBatchTests('watchlist'); }
  1799. function runTrialQuotaTests() { runBatchTests('trial-quota'); }
  1800. // 运营侧快捷批量运行
  1801. function runAdminMembershipTests() { runBatchTests('admin-membership'); }
  1802. function runAdminInviteTests() { runBatchTests('admin-invite'); }
  1803. function runAdminCrawlerTests() { runBatchTests('admin-crawler'); }
  1804. function runAdminCouponTests() { runBatchTests('admin-coupon'); }
  1805. function runAdminLicenseTests() { runBatchTests('admin-license'); }
  1806. function runAdminPaymentTests() { runBatchTests('admin-payment'); }
  1807. function runAdminAnxinTests() { runBatchTests('admin-anxin'); }
  1808. function runAdminTrialQuotaTests() { runBatchTests('admin-tq'); }
  1809. function runAdminUserTests() { runBatchTests('admin-user'); }
  1810. function runAdminAuditTests() { runBatchTests('admin-audit'); }
  1811. // ===================== 验证器 =====================
  1812. function validate_authSend(result) { return result.data?.code === 200; }
  1813. function validate_authSmslogin(result) { return result.data?.code === 200 && result.data?.data?.accessToken; }
  1814. function validate_authPwdlogin(result) { return result.data?.code === 200 && result.data?.data?.accessToken; }
  1815. function validate_authUserinfo(result) { return result.data?.code === 200 && result.data?.data?.userId; }
  1816. function validate_crawlerStatus(result) { return result.data?.code === 200 && result.data?.data?.todayQuota !== undefined; }
  1817. function validate_crawlerConsume(result) { return result.data?.code === 200; }
  1818. function validate_crawlerLogs(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1819. function validate_inviteCode(result) { return result.data?.code === 200 && result.data?.data?.code; }
  1820. function validate_inviteStats(result) { return result.data?.code === 200 && result.data?.data?.totalInvited !== undefined; }
  1821. function validate_inviteRewards(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1822. function validate_invitePage(result) { return result.data?.code === 200 && result.data?.data?.inviteCode; }
  1823. function validate_inviteLinkEntry(result) { return result.data?.code === 200 && result.data?.data?.inviteCode; }
  1824. function validate_couponMy(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1825. function validate_couponAvailable(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1826. function validate_membershipMy(result) { return result.data?.code === 200; }
  1827. function validate_membershipBenefits(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1828. function validate_membershipAdminGrant(result) { return result.data?.code === 200 && result.data?.data?.level; }
  1829. function validate_membershipAdminList(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1830. function validate_paList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1831. function validate_blInfo(result) { return result.data?.code === 200; }
  1832. function validate_blReviewList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1833. function validate_pcEnabled(result) { return result.data?.code === 200 && Array.isArray(result.data?.data) && result.data?.data?.length >= 3; }
  1834. function validate_pcDetail(result) { return result.data?.code === 200 && result.data?.data?.platformCode; }
  1835. function validate_tqActive(result) { return result.data?.code === 200 && result.data?.data; }
  1836. function validate_tqList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1837. function validate_tqCreate(result) { return result.data?.code === 200 && result.data?.data; }
  1838. function validate_tqUpdate(result) { return result.data?.code === 200 && result.data?.data; }
  1839. function validate_adminInviteConfig(result) { return result.data?.code === 200; }
  1840. function validate_adminInviteConfigs(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1841. function validate_adminGrants(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1842. function validate_adminInviteRewards(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1843. function validate_adminUserDetail(result) { return result.data?.code === 200; }
  1844. function validate_adminConversionStats(result) { return result.data?.code === 200 && result.data?.data; }
  1845. function validate_adminUserList(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1846. function validate_adminUserRole(result) { return result.data?.code === 200; }
  1847. function validate_auditQuery(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1848. function validate_auditSensitive(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1849. function validate_auditUser(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1850. function validate_auditTarget(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1851. function validate_auditStats(result) { return result.data?.code === 200 && result.data?.data?.count !== undefined; }
  1852. // 运营侧验证器
  1853. function validate_adminMembershipGrant(result) { return result.data?.code === 200 && result.data?.data?.level; }
  1854. function validate_adminMembershipList(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1855. function validate_adminInviteConfig(result) { return result.data?.code === 200; }
  1856. function validate_adminInviteConfigs(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1857. function validate_adminInviteRewards(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1858. function validate_adminInviteConversion(result) { return result.data?.code === 200 && result.data?.data; }
  1859. function validate_adminCrawlerGrant(result) { return result.data?.code === 200 && result.data?.data; }
  1860. function validate_adminCrawlerGrants(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1861. function validate_adminCrawlerUserDetail(result) { return result.data?.code === 200; }
  1862. function validate_adminCouponIssue(result) { return result.data?.code === 200; }
  1863. function validate_adminLicenseList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1864. function validate_adminPaymentCreate(result) { return result.data?.code === 200; }
  1865. function validate_adminPaymentUpdate(result) { return result.data?.code === 200; }
  1866. function validate_adminPaymentToggle(result) { return result.data?.code === 200; }
  1867. function validate_adminTqActive(result) { return result.data?.code === 200 && result.data?.data; }
  1868. function validate_adminTqList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1869. function validate_adminTqCreate(result) { return result.data?.code === 200 && result.data?.data; }
  1870. function validate_adminTqUpdate(result) { return result.data?.code === 200 && result.data?.data; }
  1871. function validate_adminUserList(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1872. function validate_adminUserRole(result) { return result.data?.code === 200; }
  1873. function validate_adminAuditQuery(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1874. function validate_adminAuditSensitive(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1875. function validate_adminAuditUser(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1876. function validate_adminAuditTarget(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1877. function validate_adminAuditStats(result) { return result.data?.code === 200; }
  1878. // 支付模块验证器
  1879. function validate_paymentPlans(result) { return result.data?.code === 200 && Array.isArray(result.data?.data) && result.data?.data?.length >= 2; }
  1880. function validate_paymentOrders(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1881. // 签约协议验证器
  1882. function validate_paymentAgreementSign(result) { return result.data?.code === 200 && result.data?.data?.agreementId; }
  1883. function validate_paymentAgreementList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1884. function validate_paymentAgreementDetail(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1885. function validate_paymentAgreementNotifyWechat(result) { const raw = result.data?._raw || result.data; return result.ok && (raw === 'success' || raw === 'SUCCESS'); }
  1886. function validate_paymentAnxinNotifyDeduct(result) { const raw = result.data?._raw || result.data; return result.ok && (raw === 'success' || raw === 'SUCCESS'); }
  1887. function validate_paymentAnxinNotifyCardChange(result) { const raw = result.data?._raw || result.data; return result.ok && (raw === 'success' || raw === 'SUCCESS'); }
  1888. // 安心付管理验证器
  1889. function validate_adminAnxinCreate(result) { return result.data?.code === 200 && result.data?.data?.cardId; }
  1890. function validate_adminAnxinList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1891. function validate_adminAnxinSync(result) { return result.data?.code === 200 && result.data?.data?.cardId; }
  1892. function validate_adminAnxinStatus(result) { return result.data?.code === 200; }
  1893. function validate_searchQuery(result) { return result.data?.code === 200 && result.data?.data?.prices !== undefined; }
  1894. function validate_searchSuggestions(result) { return result.data?.code === 200 && Array.isArray(result.data?.data) && result.data?.data?.length > 0; }
  1895. function validate_searchHistory(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1896. function validate_searchSave(result) { return result.data?.code === 200 && result.data?.data?.keyword; }
  1897. function validate_searchRecords(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1898. // 搜索记录模块验证器
  1899. function validate_srSave(result) { return result.data?.code === 200 && result.data?.data?.id && result.data?.data?.drugName; }
  1900. function validate_srList(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
  1901. function validate_srDetail(result) { return result.data?.code === 200 && result.data?.data?.id; }
  1902. // 平台会话模块验证器
  1903. function validate_psUpload(result) { return result.data?.code === 200 && result.data?.data?.platformCode; }
  1904. function validate_psList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
  1905. // ===================== 初始化 =====================
  1906. window.addEventListener('DOMContentLoaded', () => {
  1907. const params = new URLSearchParams(window.location.search);
  1908. if (params.get('baseUrl')) document.getElementById('baseUrl').value = params.get('baseUrl');
  1909. // 渲染默认模块
  1910. renderModule('auth');
  1911. });
  1912. </script>
  1913. </body>
  1914. </html>