添加mock数据
This commit is contained in:
@@ -30,6 +30,24 @@ function removeAuthStore(key : string) {
|
||||
uni.removeStorageSync(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全读取 UTSJSONObject 或普通 JS 对象的字段。
|
||||
* 微信小程序编译后 res.header / res.data / options.headers 均为普通 JS 对象,
|
||||
* 直接调用 .getString() / .set() 会抛 "not a function" 异常。
|
||||
* 此函数统一处理两种情况:先尝试 getString,不可用则 fallback 到 []。
|
||||
*/
|
||||
function safeStr(obj: any, key: string): string | null {
|
||||
if (obj == null) return null
|
||||
try {
|
||||
if (typeof obj.getString === 'function') {
|
||||
const v = obj.getString(key)
|
||||
return v != null ? String(v) : null
|
||||
}
|
||||
} catch (e) {}
|
||||
const v = obj[key]
|
||||
return (v != null && v !== '') ? String(v) : null
|
||||
}
|
||||
|
||||
let _accessToken : string | null = null;
|
||||
let _refreshToken : string | null = null;
|
||||
let _expiresAt : number | null = null;
|
||||
@@ -130,84 +148,62 @@ export class AkReq {
|
||||
static async request(options : AkReqOptions, skipRefresh ?: boolean) : Promise<AkReqResponse<any>> {
|
||||
// 自动刷新 token
|
||||
if (skipRefresh != true) {
|
||||
let apikey : string | null = null;
|
||||
const headersObj = options.headers;
|
||||
if (headersObj != null && typeof headersObj.getString === 'function') {
|
||||
apikey = headersObj.getString('apikey');
|
||||
}
|
||||
// safeStr 兼容 plain object 和 UTSJSONObject
|
||||
const apikey = safeStr(options.headers as any, 'apikey');
|
||||
await this.refreshTokenIfNeeded(apikey);
|
||||
}
|
||||
|
||||
// 构建新的 headers 对象,确保所有字段都被正确传递
|
||||
const newHeaders = new UTSJSONObject()
|
||||
|
||||
// 首先复制原始 headers
|
||||
if (options.headers != null) {
|
||||
const originalHeaders = options.headers
|
||||
if (typeof originalHeaders.getString === 'function') {
|
||||
// 复制 apikey
|
||||
const apikeyStr = originalHeaders.getString('apikey')
|
||||
if (apikeyStr != null) {
|
||||
newHeaders.set('apikey', apikeyStr)
|
||||
}
|
||||
// 复制 Content-Type
|
||||
const contentType = originalHeaders.getString('Content-Type')
|
||||
if (contentType != null) {
|
||||
newHeaders.set('Content-Type', contentType)
|
||||
}
|
||||
// 复制 Prefer
|
||||
const prefer = originalHeaders.getString('Prefer')
|
||||
if (prefer != null) {
|
||||
newHeaders.set('Prefer', prefer)
|
||||
}
|
||||
// 复制 Authorization(如果存在)
|
||||
const auth = originalHeaders.getString('Authorization')
|
||||
if (auth != null) {
|
||||
newHeaders.set('Authorization', auth)
|
||||
}
|
||||
// 构建请求 headers:用 safeStr 兼容 plain object / UTSJSONObject
|
||||
// aksupa.uts 创建的 headers 是“{ key: val } as UTSJSONObject” ,只是类型标注,运行时是 plain object
|
||||
const origHdr = options.headers as any
|
||||
const origApikey = safeStr(origHdr, 'apikey')
|
||||
const origCT = safeStr(origHdr, 'Content-Type') ?? safeStr(origHdr, 'content-type')
|
||||
const origPrefer = safeStr(origHdr, 'Prefer')
|
||||
const origAuth = safeStr(origHdr, 'Authorization') ?? safeStr(origHdr, 'authorization')
|
||||
const origRange = safeStr(origHdr, 'Range')
|
||||
const origRangeUnit = safeStr(origHdr, 'Range-Unit')
|
||||
|
||||
// apikey:优先用传入的,再 fallback 到全局 SUPA_KEY
|
||||
const finalApikey = origApikey ?? (SUPA_KEY !== '' ? SUPA_KEY : null)
|
||||
|
||||
// Authorization:Supabase Kong 要求所有请求都带 Authorization: Bearer <jwt>
|
||||
// 匿名态:Bearer <anon_key>;登录态:Bearer <access_token>
|
||||
const token = this.getToken()
|
||||
let finalAuth: string | null = origAuth
|
||||
let authMode: string = 'none'
|
||||
if (finalAuth == null) {
|
||||
if (token != null && token !== '') {
|
||||
// 登录态:用用户 access_token
|
||||
finalAuth = `Bearer ${token}`
|
||||
authMode = 'user-token'
|
||||
} else if (finalApikey != null && finalApikey !== '') {
|
||||
// 匿名态:用 anon key 作为 Bearer(Supabase Kong 必须)
|
||||
finalAuth = `Bearer ${finalApikey}`
|
||||
authMode = 'anon-key'
|
||||
}
|
||||
} else {
|
||||
authMode = 'pre-set'
|
||||
}
|
||||
|
||||
// 补齐 apikey (如果 headers 中没有,则直接使用 SUPA_KEY 补全)
|
||||
if (newHeaders.getString('apikey') == null) {
|
||||
if (SUPA_KEY != null && SUPA_KEY != "") {
|
||||
newHeaders.set('apikey', SUPA_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加/更新 Authorization
|
||||
const token = this.getToken();
|
||||
|
||||
// 检查原始 headers 中是否存在 Authorization
|
||||
let existAuth: string | null = null
|
||||
if (options.headers != null) {
|
||||
const originalHeaders = options.headers
|
||||
if (typeof originalHeaders.getString === 'function') {
|
||||
existAuth = originalHeaders.getString('Authorization')
|
||||
if (existAuth == null) {
|
||||
existAuth = originalHeaders.getString('authorization')
|
||||
}
|
||||
}
|
||||
}
|
||||
// 组装最终 header(plain object,所有平台均可正确传递)
|
||||
const currentHeaders: Record<string, string> = {}
|
||||
if (finalApikey != null && finalApikey !== '') currentHeaders['apikey'] = finalApikey
|
||||
if (finalAuth != null && finalAuth !== '') currentHeaders['Authorization'] = finalAuth
|
||||
currentHeaders['Content-Type'] = origCT ?? options.contentType ?? 'application/json'
|
||||
currentHeaders['Accept'] = 'application/json'
|
||||
if (origPrefer != null) currentHeaders['Prefer'] = origPrefer
|
||||
if (origRange != null) currentHeaders['Range'] = origRange
|
||||
if (origRangeUnit != null) currentHeaders['Range-Unit'] = origRangeUnit
|
||||
|
||||
if ((token != null && token != "") && (existAuth == null)) {
|
||||
newHeaders.set('Authorization', `Bearer ${token}`)
|
||||
}
|
||||
|
||||
// 确保 Content-Type 存在
|
||||
if (newHeaders.getString('Content-Type') == null) {
|
||||
const contentType = options.contentType ?? 'application/json'
|
||||
if (contentType != null && contentType != "") {
|
||||
newHeaders.set('Content-Type', contentType)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 Accept
|
||||
newHeaders.set('Accept', 'application/json')
|
||||
|
||||
console.log('[AkReq.request] headers:', JSON.stringify(newHeaders))
|
||||
|
||||
const currentHeaders = newHeaders
|
||||
// 请求发出前诊断日志(脱敏:前6位+后4位)
|
||||
const dbgKey = finalApikey != null && finalApikey.length > 14
|
||||
? finalApikey.substring(0, 6) + '...' + finalApikey.substring(finalApikey.length - 4)
|
||||
: (finalApikey != null ? '(short)' : '(MISSING!)')
|
||||
const dbgAuth = finalAuth != null && finalAuth.length > 15
|
||||
? finalAuth.substring(0, 13) + '...' + finalAuth.substring(finalAuth.length - 4)
|
||||
: (finalAuth != null ? finalAuth : '(MISSING!)')
|
||||
console.log('[ak-req]', (options.method ?? 'GET'), options.url)
|
||||
console.log('[ak-req] apikey:', dbgKey, '| Authorization:', dbgAuth, '| auth-mode:', authMode, '| prefer:', origPrefer ?? '(none)')
|
||||
|
||||
const timeout = options.timeout ?? 10000;
|
||||
const maxRetry = Math.max(0, options.retryCount ?? 0);
|
||||
@@ -234,15 +230,41 @@ export class AkReq {
|
||||
}
|
||||
|
||||
// 兼容 res.data 可能为 string 或 UTSJSONObject 或 UTSArray
|
||||
// 先读取响应 content-type,判断是否应该 JSON.parse
|
||||
// res.header 在 MP 环境下是普通 JS 对象(无 getString),用括号访问
|
||||
let respContentType = ''
|
||||
try {
|
||||
const hdr = res.header as any
|
||||
if (hdr != null) {
|
||||
respContentType = hdr['content-type'] ?? hdr['Content-Type'] ?? ''
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
let data : UTSJSONObject | Array<UTSJSONObject> | null;
|
||||
if (typeof res.data == 'string') {
|
||||
const strData = res.data as string;
|
||||
if (strData.length > 0 && /[^\s]/.test(strData)) {
|
||||
try {
|
||||
data = JSON.parse(strData) as UTSJSONObject;
|
||||
} catch (e) {
|
||||
// 非 JSON 响应(例如纯文本/空响应/数字等),保持原始字符串,避免 JSON.parse 崩溃
|
||||
data = new UTSJSONObject({ raw: strData });
|
||||
const looksLikeHtml = strData.trimStart().startsWith('<')
|
||||
if (looksLikeHtml || (respContentType.indexOf('text/html') >= 0)) {
|
||||
// 明确是 HTML,直接跳过 JSON.parse
|
||||
const preview = strData.substring(0, 250)
|
||||
console.error('[ak-req] non-json response (HTML)')
|
||||
console.error('[ak-req] status:', res.statusCode)
|
||||
console.error('[ak-req] content-type:', respContentType)
|
||||
console.error('[ak-req] raw preview:', preview)
|
||||
data = new UTSJSONObject({ raw: strData })
|
||||
} else {
|
||||
try {
|
||||
data = JSON.parse(strData) as UTSJSONObject;
|
||||
} catch (e) {
|
||||
// 非 JSON 响应(纯文本/数字等)
|
||||
const preview = strData.substring(0, 250)
|
||||
console.error('[ak-req] json parse failed')
|
||||
console.error('[ak-req] status:', res.statusCode)
|
||||
console.error('[ak-req] content-type:', respContentType)
|
||||
console.error('[ak-req] raw preview:', preview)
|
||||
data = new UTSJSONObject({ raw: strData });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data = null;
|
||||
@@ -250,12 +272,14 @@ export class AkReq {
|
||||
} else if (Array.isArray(res.data)) {
|
||||
data = res.data as UTSJSONObject[];
|
||||
} else {
|
||||
const objData = res.data as UTSJSONObject | null;
|
||||
data = objData;
|
||||
// MP 下 res.data 可能是 plain JS object(平台已预解析 JSON),用 safeStr 兼容
|
||||
const objData = res.data as any;
|
||||
data = objData as UTSJSONObject;
|
||||
if (objData != null) {
|
||||
const accessToken = objData.getString('access_token');
|
||||
const refreshTokenNew = objData.getString('refresh_token');
|
||||
const expiresAt = objData.getNumber('expires_at');
|
||||
const accessToken = safeStr(objData, 'access_token')
|
||||
const refreshTokenNew = safeStr(objData, 'refresh_token')
|
||||
const expiresAtRaw = objData['expires_at']
|
||||
const expiresAt = expiresAtRaw != null ? Number(expiresAtRaw) : null
|
||||
if (accessToken !== null && refreshTokenNew !== null && expiresAt !== null) {
|
||||
AkReq.setToken(accessToken, refreshTokenNew, expiresAt);
|
||||
}
|
||||
@@ -298,26 +322,47 @@ export class AkReq {
|
||||
attempt++;
|
||||
}
|
||||
const finalRes = lastRes!!;
|
||||
// 全局处理 401 未授权:在非 refresh 场景下,清理 token。
|
||||
// 测试模式下不强制跳登录页,避免影响任意跳转调试。
|
||||
// 全局处理 401 未授权
|
||||
if ((finalRes.status === 401) && (skipRefresh !== true)) {
|
||||
uni.$emit('AUTH_SESSION_EXPIRED', { reason: '401' });
|
||||
// 401 诊断日志:区分"前端 header 问题"vs"服务端 key/实例问题"
|
||||
const sentApikey = (currentHeaders as any)['apikey']
|
||||
const sentAuth = (currentHeaders as any)['Authorization']
|
||||
const dbgSentKey = sentApikey != null && sentApikey.length > 14
|
||||
? sentApikey.substring(0, 6) + '...' + sentApikey.substring(sentApikey.length - 4)
|
||||
: (sentApikey != null ? '(short)' : '(MISSING!)')
|
||||
const dbgSentAuth = sentAuth != null && sentAuth.length > 15
|
||||
? sentAuth.substring(0, 13) + '...' + sentAuth.substring(sentAuth.length - 4)
|
||||
: (sentAuth != null ? '(short)' : '(MISSING!)')
|
||||
console.error('[ak-req] ★ 401 Unauthorized')
|
||||
console.error('[ak-req] url:', options.url)
|
||||
console.error('[ak-req] auth-mode: ' + authMode)
|
||||
console.error('[ak-req] 发送 apikey:', dbgSentKey)
|
||||
console.error('[ak-req] 发送 Authorization:', dbgSentAuth)
|
||||
console.error('[ak-req] response body:', JSON.stringify(finalRes.data))
|
||||
// 前端 header 检查
|
||||
if (sentApikey == null || sentApikey === '') {
|
||||
console.error('[ak-req] ✗ 前端问题:apikey 未发送,检查 SUPA_KEY 是否已配置')
|
||||
} else if (sentAuth == null || sentAuth === '') {
|
||||
console.error('[ak-req] ✗ 前端问题:Authorization 未发送(本次修复后不应再出现此情况)')
|
||||
} else {
|
||||
console.error('[ak-req] ✓ 前端 header 已正确发送,401 来自服务端')
|
||||
console.error('[ak-req] 请运维核查以下服务端配置:')
|
||||
console.error('[ak-req] 1. SUPA_KEY 是否属于 9126 这个实例(不同实例 key 不通用)')
|
||||
console.error('[ak-req] 2. 9126 和 9127 是否为同一套 docker-compose / 同一实例')
|
||||
console.error('[ak-req] 3. Kong 是否已 reload 到最新 consumer/key 配置')
|
||||
console.error('[ak-req] 4. Supabase 实例的 ANON_KEY 环境变量与 SUPA_KEY 是否一致')
|
||||
console.error('[ak-req] 5. categories/ml_brands/ml_member_levels 是否在该实例 public schema 下')
|
||||
console.error('[ak-req] 6. RLS policy 是否允许 anon role SELECT')
|
||||
}
|
||||
|
||||
uni.$emit('AUTH_SESSION_EXPIRED', { reason: '401' });
|
||||
try {
|
||||
this.clearToken();
|
||||
uni.showToast({ title: '未授权或登录已过期,请重新登录', icon: 'none' });
|
||||
// IS_TEST_MODE=true 时不 showToast 和跳转,避免干扰调试
|
||||
if (IS_TEST_MODE !== true) {
|
||||
uni.showToast({ title: '未授权或登录已过期,请重新登录', icon: 'none' });
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
// 动态读取配置,避免 ak-req 模块与业务工程强耦合
|
||||
// const cfg = require('@/ak/config.uts') as any
|
||||
// const isTest = cfg != null ? (cfg.IS_TEST_MODE === true) : false
|
||||
const isTest = IS_TEST_MODE
|
||||
// if (!isTest) {
|
||||
// uni.reLaunch({ url: '/pages/user/login' });
|
||||
// }
|
||||
} catch (e) {
|
||||
// try { uni.reLaunch({ url: '/pages/user/login' }); } catch (e2) {}
|
||||
}
|
||||
}
|
||||
return finalRes;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user