添加mock数据

This commit is contained in:
2026-04-13 11:32:31 +08:00
parent 334e5936c9
commit 37141c1d6b
17 changed files with 1843 additions and 330 deletions

View File

@@ -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)
// AuthorizationSupabase 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 作为 BearerSupabase 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')
}
}
}
// 组装最终 headerplain 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;
}