|
|
@@ -192,6 +192,7 @@
|
|
|
<div class="module-tab" data-module="activity-available">5.我可参加的活动</div>
|
|
|
<div class="module-tab" data-module="activity-rewards-tab">6.各种活动奖励</div>
|
|
|
<div class="module-tab" data-module="search-record">7.搜索记录</div>
|
|
|
+ <div class="module-tab" data-module="external-login">8.第三方登录</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 运营侧模块标签栏 -->
|
|
|
@@ -201,6 +202,7 @@
|
|
|
<div class="module-tab" data-module="admin-activity-checkin">3.签到送会员管理</div>
|
|
|
<div class="module-tab" data-module="admin-activity-lottery">4.比价抽奖管理</div>
|
|
|
<div class="module-tab" data-module="admin-license">5.入驻信息审核</div>
|
|
|
+ <div class="module-tab" data-module="admin-system-config">6.系统配置管理</div>
|
|
|
</div>
|
|
|
|
|
|
<div id="panels"></div>
|
|
|
@@ -218,8 +220,8 @@ const SMS_CODE = () => document.getElementById('smsCode')?.value || '123456';
|
|
|
|
|
|
let stats = { pass: 0, fail: 0, pending: 0, total: 0 };
|
|
|
|
|
|
-const USER_MODULES = ['checkin', 'activity-invite', 'activity-lottery', 'business-license', 'activity-available', 'activity-rewards-tab', 'search-record'];
|
|
|
-const ADMIN_MODULES = ['admin-checkin', 'admin-activity-invite', 'admin-activity-checkin', 'admin-activity-lottery', 'admin-license'];
|
|
|
+const USER_MODULES = ['checkin', 'activity-invite', 'activity-lottery', 'business-license', 'activity-available', 'activity-rewards-tab', 'search-record', 'external-login'];
|
|
|
+const ADMIN_MODULES = ['admin-checkin', 'admin-activity-invite', 'admin-activity-checkin', 'admin-activity-lottery', 'admin-license', 'admin-system-config'];
|
|
|
|
|
|
// ===================== 大整数安全JSON解析 =====================
|
|
|
function safeJsonParse(text) {
|
|
|
@@ -440,11 +442,13 @@ function renderModule(module) {
|
|
|
case 'activity-available': html = renderActivityAvailableModule(); break;
|
|
|
case 'activity-rewards-tab': html = renderActivityRewardsModule(); break;
|
|
|
case 'search-record': html = renderSearchRecordModule(); break;
|
|
|
+ case 'external-login': html = renderExternalLoginModule(); break;
|
|
|
case 'admin-checkin': html = renderAdminCheckinModule(); break;
|
|
|
case 'admin-activity-invite': html = renderAdminActivityInviteModule(); break;
|
|
|
case 'admin-activity-checkin': html = renderAdminActivityCheckinModule(); break;
|
|
|
case 'admin-activity-lottery': html = renderAdminActivityLotteryModule(); break;
|
|
|
case 'admin-license': html = renderAdminLicenseModule(); break;
|
|
|
+ case 'admin-system-config': html = renderAdminSystemConfigModule(); break;
|
|
|
}
|
|
|
panels.innerHTML = html;
|
|
|
panels.querySelectorAll('input[data-raw]').forEach(input => {
|
|
|
@@ -793,6 +797,48 @@ const FIELD_DESC = {
|
|
|
['data.prices[].stockStatus', 'String', '库存状态(IN_STOCK/LOW_STOCK/OUT_OF_STOCK)'],
|
|
|
['data.prices[].isLowest', 'Boolean', '是否为最低价'],
|
|
|
['data.prices[].purchaseUrl', 'String', '平台跳转链接']
|
|
|
+ ],
|
|
|
+ 'externalLoginResponse': [
|
|
|
+ ['data.userId', 'String', '用户ID(字符串,避免JS大数精度丢失)'],
|
|
|
+ ['data.accessToken', 'String', '访问令牌'],
|
|
|
+ ['data.refreshToken', 'String', '刷新令牌'],
|
|
|
+ ['data.expiresIn', 'Long', 'accessToken有效期(秒),默认7200'],
|
|
|
+ ['data.newUser', 'Boolean', '是否为新注册用户'],
|
|
|
+ ['data.needBindPhone', 'Boolean', '始终返回false'],
|
|
|
+ ['data.role', 'String', '固定值"USER"'],
|
|
|
+ ['data.userInfo.id', 'String', '用户ID'],
|
|
|
+ ['data.userInfo.phone', 'String', '手机号(脱敏)'],
|
|
|
+ ['data.userInfo.nickname', 'String', '昵称(即第三方传入的username或nickname)'],
|
|
|
+ ['data.userInfo.avatar', 'String', '头像URL'],
|
|
|
+ ['data.userInfo.wechatBound', 'Boolean', '是否已绑定微信'],
|
|
|
+ ['data.userInfo.loginSource', 'String', '登录来源(MINIAPP)'],
|
|
|
+ ['data.userInfo.membershipLevel', 'String', '会员等级代码'],
|
|
|
+ ['data.userInfo.membershipLevelName', 'String', '会员等级名称'],
|
|
|
+ ['data.userInfo.createTime', 'String', '注册时间']
|
|
|
+ ],
|
|
|
+ 'systemConfig': [
|
|
|
+ ['data.id', 'Long', '配置ID'],
|
|
|
+ ['data.configKey', 'String', '配置键(语义化标识,如 miniapp_external_api,程序通过此key定位配置)'],
|
|
|
+ ['data.configValue', 'String', '配置值(JSON格式,包含该配置项所有参数,如{"apiKey":"xxx","allowedIps":"..."})'],
|
|
|
+ ['data.configType', 'String', '配置类型:EXTERNAL_API / GENERAL'],
|
|
|
+ ['data.configName', 'String', '配置名称(中文描述)'],
|
|
|
+ ['data.enabled', 'Boolean', '是否启用'],
|
|
|
+ ['data.remark', 'String', '备注'],
|
|
|
+ ['data.createTime', 'DateTime', '创建时间'],
|
|
|
+ ['data.updateTime', 'DateTime', '更新时间']
|
|
|
+ ],
|
|
|
+ 'systemConfigList': [
|
|
|
+ ['data.records[].id', 'Long', '配置ID'],
|
|
|
+ ['data.records[].configKey', 'String', '配置键'],
|
|
|
+ ['data.records[].configValue', 'String', '配置值JSON'],
|
|
|
+ ['data.records[].configType', 'String', '配置类型'],
|
|
|
+ ['data.records[].configName', 'String', '配置名称'],
|
|
|
+ ['data.records[].enabled', 'Boolean', '是否启用'],
|
|
|
+ ['data.records[].remark', 'String', '备注'],
|
|
|
+ ['data.total', 'Long', '总记录数'],
|
|
|
+ ['data.current', 'Long', '当前页'],
|
|
|
+ ['data.size', 'Long', '每页条数'],
|
|
|
+ ['data.pages', 'Long', '总页数']
|
|
|
]
|
|
|
};
|
|
|
|
|
|
@@ -1053,6 +1099,61 @@ function renderSearchRecordModule() {
|
|
|
${testSection('sr-clear', '清空记录', 'DELETE', 'delete', '/api/search-record/history', '需认证', 'auth-required', null, false, null, '清空当前用户全部搜索记录')}`;
|
|
|
}
|
|
|
|
|
|
+// ==================== 用户端:第三方登录模块(API Key鉴权) ====================
|
|
|
+function renderExternalLoginModule() {
|
|
|
+ return `
|
|
|
+ <div class="batch-buttons">
|
|
|
+ <button class="btn btn-warning btn-sm" onclick="runExternalLoginTests()">运行第三方登录测试</button>
|
|
|
+ </div>
|
|
|
+ <div class="test-section">
|
|
|
+ <div class="section-title">
|
|
|
+ <span class="method method-post">POST</span>
|
|
|
+ <span class="path">/api/auth/external/miniapp-token</span>
|
|
|
+ <span class="side-tag side-user">用户侧</span>
|
|
|
+ <span class="auth-public auth-tag">公开(API Key)</span>
|
|
|
+ </div>
|
|
|
+ <div class="section-desc">第三方系统通过API Key换取用户登录Token(免密登录)。新用户自动注册并初始化PLUS会员,已存在用户更新传入信息。鉴权方式:请求头 X-Api-Key</div>
|
|
|
+ <details class="field-desc-details">
|
|
|
+ <summary class="field-desc-summary">📞 cURL 调用示例</summary>
|
|
|
+ <pre class="curl-example"># 第三方系统通过API Key换取Token:
|
|
|
+curl -X POST "$" + "{BASE_URL}/api/auth/external/miniapp-token" \\
|
|
|
+ -H "Content-Type: application/json" \\
|
|
|
+ -H "X-Api-Key: $" + "{API_KEY}" \\
|
|
|
+ -d '{"phone":"13800138000","username":"yaohuigou001","pharmacyName":"XX大药房","province":"广东省","city":"深圳市","district":"南山区"}'</pre>
|
|
|
+ </details>
|
|
|
+ ${renderFieldDesc('externalLoginResponse')}
|
|
|
+ <div class="form-grid">
|
|
|
+ <div class="form-group"><label>API Key(X-Api-Key) *</label><input type="text" id="external-login-apikey" placeholder="第三方系统的API Key" value=""></div>
|
|
|
+ <div class="form-group"><label>手机号(phone) *</label><input type="text" id="external-login-phone" placeholder="13800138000" value="18776156917"></div>
|
|
|
+ <div class="form-group"><label>邀请码(inviteCode)</label><input type="text" id="external-login-inviteCode" placeholder="仅新用户注册时生效" value=""></div>
|
|
|
+ <div class="form-group"><label>用户名(username)</label><input type="text" id="external-login-username" placeholder="保存到nickname字段" value="yaohuigou001"></div>
|
|
|
+ <div class="form-group"><label>昵称(nickname)</label><input type="text" id="external-login-nickname" placeholder="优先级低于username" value=""></div>
|
|
|
+ <div class="form-group"><label>药店名称(pharmacyName)</label><input type="text" id="external-login-pharmacyName" placeholder="XX大药房" value=""></div>
|
|
|
+ <div class="form-group"><label>省(province)</label><input type="text" id="external-login-province" placeholder="广东省" value=""></div>
|
|
|
+ <div class="form-group"><label>市(city)</label><input type="text" id="external-login-city" placeholder="深圳市" value=""></div>
|
|
|
+ <div class="form-group"><label>区(district)</label><input type="text" id="external-login-district" placeholder="南山区" value=""></div>
|
|
|
+ <div class="form-group"><label>药店地址(pharmacyAddress)</label><input type="text" id="external-login-pharmacyAddress" placeholder="科技园路1号" value=""></div>
|
|
|
+ <div class="form-group"><label>联系人(contactPerson)</label><input type="text" id="external-login-contactPerson" placeholder="张三" value=""></div>
|
|
|
+ <div class="form-group"><label>联系电话(contactPhone)</label><input type="text" id="external-login-contactPhone" placeholder="13800138000" value=""></div>
|
|
|
+ <div class="form-group"><label>营业执照号(businessLicenseNo)</label><input type="text" id="external-login-businessLicenseNo" placeholder="91110105MA12345678" value=""></div>
|
|
|
+ <div class="form-group"><label>店铺名称(storeName)</label><input type="text" id="external-login-storeName" placeholder="入驻信息专用,不传则用pharmacyName" value=""></div>
|
|
|
+ <div class="form-group"><label>终端类型(terminalType)</label><input type="text" id="external-login-terminalType" placeholder="SINGLE/CHAIN/CLINIC/COMMUNITY_HEALTH" value=""></div>
|
|
|
+ <div class="form-group"><label>营业执照图片(businessLicenseUrl)</label><input type="text" id="external-login-businessLicenseUrl" placeholder="https://oss.example.com/yyzz.jpg" value=""></div>
|
|
|
+ <div class="form-group"><label>药品经营许可证(drugLicenseUrl)</label><input type="text" id="external-login-drugLicenseUrl" placeholder="https://oss.example.com/ypjy.jpg" value=""></div>
|
|
|
+ <div class="form-group"><label>二类医疗器械备案(medicalDeviceClass2Url)</label><input type="text" id="external-login-medicalDeviceClass2Url" placeholder="https://oss.example.com/elqx.jpg" value=""></div>
|
|
|
+ <div class="form-group"><label>三类医疗器械备案(medicalDeviceClass3Url)</label><input type="text" id="external-login-medicalDeviceClass3Url" placeholder="https://oss.example.com/slqx.jpg" value=""></div>
|
|
|
+ </div>
|
|
|
+ <button class="btn btn-primary" onclick="runExternalMiniappToken()">执行测试</button>
|
|
|
+ <div class="response-box" id="external-login-box" style="display:none;">
|
|
|
+ <div class="response-header">
|
|
|
+ <span class="response-title">响应 <span id="external-login-duration" style="color:#999;font-weight:400;"></span></span>
|
|
|
+ <span class="status-badge" id="external-login-status"></span>
|
|
|
+ </div>
|
|
|
+ <div class="response-content" id="external-login-raw"></div>
|
|
|
+ </div>
|
|
|
+ </div>`;
|
|
|
+}
|
|
|
+
|
|
|
// ==================== 运营端:签到管理模块 ====================
|
|
|
function renderAdminCheckinModule() {
|
|
|
return `
|
|
|
@@ -1236,6 +1337,47 @@ function renderAdminLicenseModule() {
|
|
|
{label:'审核动作(action)', name:'action', placeholder:'REJECTED', default:'REJECTED'},
|
|
|
{label:'拒绝原因(rejectReason)', name:'rejectReason', placeholder:'图片不清晰', default:''}
|
|
|
], true, null, '拒绝入驻信息申请,需填写拒绝原因供申请人查看')}`;
|
|
|
+};
|
|
|
+
|
|
|
+// ==================== 运营端:系统配置管理模块 ====================
|
|
|
+function renderAdminSystemConfigModule() {
|
|
|
+ return `
|
|
|
+ <div class="batch-buttons">
|
|
|
+ <button class="btn btn-warning btn-sm" onclick="runAdminSystemConfigTests()">运行全部系统配置测试</button>
|
|
|
+ </div>
|
|
|
+ ${testSection('admin-system-config-list', '分页查询配置列表', 'GET', 'get', '/api/admin/system-config/list?configType={configType}&page={page}&size={size}', '需认证', 'auth-required',
|
|
|
+ [
|
|
|
+ {label:'配置类型(configType,可选)', name:'configType', placeholder:'EXTERNAL_API / GENERAL', default:''},
|
|
|
+ {label:'页码(page)', name:'page', placeholder:'1', default:'1'},
|
|
|
+ {label:'每页(size)', name:'size', placeholder:'20', default:'20'}
|
|
|
+ ], true, 'systemConfigList', '分页查询系统配置列表,支持按configType筛选。configType可选值:EXTERNAL_API-外部API鉴权 / GENERAL-通用配置')}
|
|
|
+ ${testSection('admin-system-config-enabled', '查询启用的配置', 'GET', 'get', '/api/admin/system-config/enabled?configType={configType}', '需认证', 'auth-required',
|
|
|
+ [{label:'配置类型(configType,可选)', name:'configType', placeholder:'EXTERNAL_API / GENERAL', default:''}], true, 'systemConfig', '查询所有启用的配置列表(不分页),支持按configType筛选')}
|
|
|
+ ${testSection('admin-system-config-by-id', '按ID查询配置', 'GET', 'get', '/api/admin/system-config/{id}', '需认证', 'auth-required',
|
|
|
+ [{label:'配置ID(id)', name:'id', placeholder:'1', default:'1'}], true, 'systemConfig', '根据主键ID查询单条配置详情')}
|
|
|
+ ${testSection('admin-system-config-by-key', '按Key查询配置', 'GET', 'get', '/api/admin/system-config/by-key?key={key}', '需认证', 'auth-required',
|
|
|
+ [{label:'配置键(key)', name:'key', placeholder:'API Key字符串', default:''}], true, 'systemConfig', '根据configKey精确查询配置。EXTERNAL_API类型时key即为API Key本身')}
|
|
|
+ ${testSection('admin-system-config-create', '新增配置', 'POST', 'post', '/api/admin/system-config', '需认证', 'auth-required',
|
|
|
+ [
|
|
|
+ {label:'配置键(configKey) *', name:'configKey', placeholder:'API Key或自定义Key', default:'test-api-key-001'},
|
|
|
+ {label:'配置值(configValue)', name:'configValue', placeholder:'{"allowedIps":"192.168.1.1,10.0.0.2"}', default:'{"allowedIps":""}'},
|
|
|
+ {label:'配置类型(configType) *', name:'configType', placeholder:'EXTERNAL_API', default:'EXTERNAL_API'},
|
|
|
+ {label:'配置名称(configName) *', name:'configName', placeholder:'第三方API鉴权配置', default:'测试API鉴权配置'},
|
|
|
+ {label:'启用(enabled)', name:'enabled', placeholder:'true/false', default:'true', type:'string'},
|
|
|
+ {label:'备注(remark)', name:'remark', placeholder:'可选备注', default:'测试用配置'}
|
|
|
+ ], true, 'systemConfig', '新增一条系统配置。EXTERNAL_API类型:configKey填API Key,configValue填JSON如{"allowedIps":"192.168.1.1"},ip留空表示不限制IP')}
|
|
|
+ ${testSection('admin-system-config-update', '更新配置', 'PUT', 'put', '/api/admin/system-config/{id}', '需认证', 'auth-required',
|
|
|
+ [
|
|
|
+ {label:'配置ID(id)', name:'id', placeholder:'1', default:''},
|
|
|
+ {label:'配置键(configKey)', name:'configKey', placeholder:'API Key或自定义Key', default:''},
|
|
|
+ {label:'配置值(configValue)', name:'configValue', placeholder:'{"allowedIps":"192.168.1.1"}', default:'{"allowedIps":"192.168.1.1"}'},
|
|
|
+ {label:'配置类型(configType)', name:'configType', placeholder:'EXTERNAL_API', default:''},
|
|
|
+ {label:'配置名称(configName)', name:'configName', placeholder:'第三方API鉴权配置', default:''},
|
|
|
+ {label:'启用(enabled)', name:'enabled', placeholder:'true/false', default:'', type:'string'},
|
|
|
+ {label:'备注(remark)', name:'remark', placeholder:'可选备注', default:''}
|
|
|
+ ], true, 'systemConfig', '更新指定ID的系统配置。修改IP白名单后将立即生效(无需重启)')}
|
|
|
+ ${testSection('admin-system-config-delete', '删除配置', 'DELETE', 'delete', '/api/admin/system-config/{id}', '需认证', 'auth-required',
|
|
|
+ [{label:'配置ID(id)', name:'id', placeholder:'需删除的配置ID', default:''}], true, null, '删除指定ID的系统配置。删除后对应API Key将立即失效')}`;
|
|
|
}
|
|
|
|
|
|
// ===================== 批量测试 ====================
|
|
|
@@ -1403,6 +1545,7 @@ function runAdminActivityInviteTests() { runBatchTests('admin-activity-invite');
|
|
|
function runAdminActivityCheckinTests() { runBatchTests('admin-activity-checkin'); }
|
|
|
function runAdminActivityLotteryTests() { runBatchTests('admin-activity-lottery'); }
|
|
|
function runAdminLicenseTests() { runBatchTests('admin-license'); }
|
|
|
+function runAdminSystemConfigTests() { runBatchTests('admin-system-config'); }
|
|
|
function runSearchRecordTests() { runBatchTests('search-record'); }
|
|
|
|
|
|
// ===================== 验证器 =====================
|
|
|
@@ -1448,6 +1591,15 @@ function validate_adminLicenseList(result) { return result.data?.code === 200 &&
|
|
|
function validate_adminLicenseApprove(result) { return result.data?.code === 200; }
|
|
|
function validate_adminLicenseReject(result) { return result.data?.code === 200; }
|
|
|
|
|
|
+// 系统配置管理模块
|
|
|
+function validate_adminSystemConfigList(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
|
|
|
+function validate_adminSystemConfigEnabled(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
|
|
|
+function validate_adminSystemConfigById(result) { return result.data?.code === 200 && result.data?.data?.id; }
|
|
|
+function validate_adminSystemConfigByKey(result) { return result.data?.code === 200 && result.data?.data?.id; }
|
|
|
+function validate_adminSystemConfigCreate(result) { return result.data?.code === 200 && result.data?.data?.id; }
|
|
|
+function validate_adminSystemConfigUpdate(result) { return result.data?.code === 200 && result.data?.data?.id; }
|
|
|
+function validate_adminSystemConfigDelete(result) { return result.data?.code === 200; }
|
|
|
+
|
|
|
// 搜索记录模块
|
|
|
function validate_srSave(result) { return result.data?.code === 200 && result.data?.data?.id; }
|
|
|
function validate_srList(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
|
|
|
@@ -1455,6 +1607,67 @@ function validate_srDetail(result) { return result.data?.code === 200 && result.
|
|
|
function validate_srLotteryStatus(result) { return result.data?.code === 200 && result.data?.data?.enabled !== undefined; }
|
|
|
function validate_srLotteryDraw(result) { return result.data?.code === 200 && result.data?.data?.prizeName !== undefined; }
|
|
|
|
|
|
+// 第三方登录模块
|
|
|
+function validate_externalLogin(result) { return result.data?.code === 200 && result.data?.data?.accessToken; }
|
|
|
+
|
|
|
+async function runExternalMiniappToken() {
|
|
|
+ const sectionId = 'external-login';
|
|
|
+ const apiKey = document.getElementById(sectionId + '-apikey').value;
|
|
|
+ if (!apiKey) { alert('请填写 API Key'); return; }
|
|
|
+
|
|
|
+ const fieldNames = ['phone', 'inviteCode', 'username', 'nickname', 'pharmacyName', 'province', 'city', 'district', 'pharmacyAddress', 'contactPerson', 'contactPhone', 'businessLicenseNo', 'storeName', 'terminalType', 'businessLicenseUrl', 'drugLicenseUrl', 'medicalDeviceClass2Url', 'medicalDeviceClass3Url'];
|
|
|
+ const body = {};
|
|
|
+ fieldNames.forEach(f => {
|
|
|
+ const val = document.getElementById(sectionId + '-' + f).value;
|
|
|
+ if (val) body[f] = val;
|
|
|
+ });
|
|
|
+ if (!body.phone) { alert('请填写手机号'); return; }
|
|
|
+
|
|
|
+ const box = document.getElementById(sectionId + '-box');
|
|
|
+ const statusEl = document.getElementById(sectionId + '-status');
|
|
|
+ const rawEl = document.getElementById(sectionId + '-raw');
|
|
|
+ const durEl = document.getElementById(sectionId + '-duration');
|
|
|
+
|
|
|
+ updateBadge(statusEl, 'pending', '运行中...');
|
|
|
+ if (box) box.style.display = 'block';
|
|
|
+
|
|
|
+ if (!stats.hasOwnProperty(sectionId)) {
|
|
|
+ stats.total++;
|
|
|
+ stats.pending = Math.max(0, stats.total - stats.pass - stats.fail);
|
|
|
+ document.getElementById('totalCount').textContent = stats.total;
|
|
|
+ document.getElementById('pendingCount').textContent = stats.pending;
|
|
|
+ }
|
|
|
+
|
|
|
+ const startTime = Date.now();
|
|
|
+ try {
|
|
|
+ const res = await fetch(BASE_URL() + '/api/auth/external/miniapp-token', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json', 'X-Api-Key': apiKey },
|
|
|
+ body: JSON.stringify(body)
|
|
|
+ });
|
|
|
+ const text = await res.text();
|
|
|
+ let data;
|
|
|
+ try { data = safeJsonParse(text); } catch (e) { data = { _raw: text }; }
|
|
|
+ const result = { ok: res.ok, status: res.status, data, duration: Date.now() - startTime };
|
|
|
+
|
|
|
+ if (durEl) durEl.textContent = result.duration + 'ms';
|
|
|
+ if (rawEl) rawEl.textContent = JSON.stringify(result.data, null, 2);
|
|
|
+
|
|
|
+ const passed = validate_externalLogin(result);
|
|
|
+ updateStats(sectionId, passed);
|
|
|
+ updateBadge(statusEl, passed ? 'success' : 'error', passed ? '通过 ' + result.status : '失败 ' + result.status);
|
|
|
+ } catch (e) {
|
|
|
+ const dur = Date.now() - startTime;
|
|
|
+ if (durEl) durEl.textContent = dur + 'ms';
|
|
|
+ if (rawEl) rawEl.textContent = JSON.stringify({ error: e.message }, null, 2);
|
|
|
+ updateStats(sectionId, false);
|
|
|
+ updateBadge(statusEl, 'error', '失败 ' + e.message);
|
|
|
+ }
|
|
|
+ if (box) box.style.display = 'block';
|
|
|
+}
|
|
|
+
|
|
|
+function runExternalLoginTests() { runExternalMiniappToken(); }
|
|
|
+
|
|
|
// ===================== 初始化 =====================
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
|
const params = new URLSearchParams(window.location.search);
|