|
@@ -5,6 +5,47 @@ const BASE_URL = config.API_BASE_URL
|
|
|
var _redirectingToLogin = false
|
|
var _redirectingToLogin = false
|
|
|
var _reloginLock = false
|
|
var _reloginLock = false
|
|
|
|
|
|
|
|
|
|
+// === 请求去重(Dedup)全局安全网 ===
|
|
|
|
|
+// 同 URL + method + body 在 DEDUP_WINDOW_MS 窗口内复用同一个 Promise,
|
|
|
|
|
+// 防止 onLoad/onShow 双触发、组件嵌套、双击等场景导致的重复请求。
|
|
|
|
|
+// 401 续期重放请求传 options._skipDedup=true 跳过去重。
|
|
|
|
|
+var _dedupMap = {}
|
|
|
|
|
+var DEDUP_WINDOW_MS = 3000
|
|
|
|
|
+
|
|
|
|
|
+function _makeDedupKey(method, url, body) {
|
|
|
|
|
+ var bodyStr = ''
|
|
|
|
|
+ try { bodyStr = JSON.stringify(body || {}) } catch (e) { bodyStr = '' }
|
|
|
|
|
+ return method + '|' + url + '|' + bodyStr
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function _tryDedup(method, fullUrl, body) {
|
|
|
|
|
+ var key = _makeDedupKey(method, fullUrl, body)
|
|
|
|
|
+ var cached = _dedupMap[key]
|
|
|
|
|
+ if (cached && Date.now() - cached.ts < DEDUP_WINDOW_MS) {
|
|
|
|
|
+ return cached.promise
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function _registerDedup(method, fullUrl, body, promise) {
|
|
|
|
|
+ var key = _makeDedupKey(method, fullUrl, body)
|
|
|
|
|
+ _dedupMap[key] = { promise: promise, ts: Date.now() }
|
|
|
|
|
+ var _cleanup = function() {
|
|
|
|
|
+ // 窗口到期后清理,确保缓存不常驻
|
|
|
|
|
+ setTimeout(function() {
|
|
|
|
|
+ if (_dedupMap[key] && _dedupMap[key].promise === promise) {
|
|
|
|
|
+ delete _dedupMap[key]
|
|
|
|
|
+ }
|
|
|
|
|
+ }, DEDUP_WINDOW_MS)
|
|
|
|
|
+ }
|
|
|
|
|
+ promise.then(_cleanup, _cleanup)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function _clearDedup() {
|
|
|
|
|
+ _dedupMap = {}
|
|
|
|
|
+}
|
|
|
|
|
+export { _clearDedup as clearApiDedup }
|
|
|
|
|
+
|
|
|
function _clearAuthAndRedirect() {
|
|
function _clearAuthAndRedirect() {
|
|
|
if (_redirectingToLogin) return
|
|
if (_redirectingToLogin) return
|
|
|
_redirectingToLogin = true
|
|
_redirectingToLogin = true
|
|
@@ -73,7 +114,9 @@ function _handle401(url, method, data, options, resolve, reject, resData) {
|
|
|
console.warn('[API] 401 静默续期成功,重放原请求 ' + url)
|
|
console.warn('[API] 401 静默续期成功,重放原请求 ' + url)
|
|
|
_applyLoginData(res.data)
|
|
_applyLoginData(res.data)
|
|
|
// 重放原请求;重试期间保持锁,防止重试仍 401 时再次触发续期
|
|
// 重放原请求;重试期间保持锁,防止重试仍 401 时再次触发续期
|
|
|
- request(url, method, data, options).then(function(r) {
|
|
|
|
|
|
|
+ // 401 重放请求跳过去重(原请求已 reject,缓存中的 promise 已过期)
|
|
|
|
|
+ var _retryOpts = Object.assign({}, options, { _skipDedup: true })
|
|
|
|
|
+ request(url, method, data, _retryOpts).then(function(r) {
|
|
|
_reloginLock = false
|
|
_reloginLock = false
|
|
|
resolve(r)
|
|
resolve(r)
|
|
|
}, function(e) {
|
|
}, function(e) {
|
|
@@ -121,8 +164,18 @@ const request = (url, method = 'POST', data = {}, options = {}) => {
|
|
|
}
|
|
}
|
|
|
if (!hasFiltered) body = data
|
|
if (!hasFiltered) body = data
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // === 请求去重:同 URL + method + body 在 3 秒窗口内复用 Promise ===
|
|
|
|
|
+ // 防止 onLoad/onShow 双触发、组件嵌套、双击等导致的重复请求
|
|
|
|
|
+ if (!options._skipDedup) {
|
|
|
|
|
+ var _cached = _tryDedup(method, fullUrl, body)
|
|
|
|
|
+ if (_cached) {
|
|
|
|
|
+ // console.log('[API] dedup hit ' + method + ' ' + fullUrl)
|
|
|
|
|
+ return _cached
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var _promise = new Promise((resolve, reject) => {
|
|
|
uni.request({
|
|
uni.request({
|
|
|
url: fullUrl,
|
|
url: fullUrl,
|
|
|
method: method,
|
|
method: method,
|
|
@@ -171,6 +224,13 @@ const request = (url, method = 'POST', data = {}, options = {}) => {
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+ // 注册去重缓存,3 秒窗口内复用
|
|
|
|
|
+ if (!options._skipDedup) {
|
|
|
|
|
+ _registerDedup(method, fullUrl, body, _promise)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return _promise
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 登录模块
|
|
// 登录模块
|
|
@@ -419,9 +479,10 @@ export const getAuthorizations = () => {
|
|
|
return request('/api/parent/authorizations', 'POST')
|
|
return request('/api/parent/authorizations', 'POST')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export const getChildren = () => {
|
|
|
|
|
- return request('/api/family/member/list', 'POST', {})
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// @deprecated 别名:统一使用 getFamilyMemberList,避免同 URL 多封装导致重复调用
|
|
|
|
|
+// @deprecated 使用 getFamilyMemberList 代替。getChildren 与 getFamilyMemberList 调用同一接口 /api/family/member/list,
|
|
|
|
|
+// 同一页面同时调用两者会导致重复请求。新代码请直接使用 getFamilyMemberList。
|
|
|
|
|
+export const getChildren = () => getFamilyMemberList({})
|
|
|
|
|
|
|
|
export const getParentDashboard = () => {
|
|
export const getParentDashboard = () => {
|
|
|
return request('/api/parent/dashboard', 'POST')
|
|
return request('/api/parent/dashboard', 'POST')
|
|
@@ -515,8 +576,10 @@ export const reviewTask = (taskId, data) => {
|
|
|
return request(`/api/tasks/${taskId}/review`, 'POST', data)
|
|
return request(`/api/tasks/${taskId}/review`, 'POST', data)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export const getTaskHistory = (memberId, page = 1, size = 10) => {
|
|
|
|
|
- return request('/api/tasks/history', 'POST', { memberId, page, size })
|
|
|
|
|
|
|
+export const getTaskHistory = (memberId, page = 1, size = 10, category) => {
|
|
|
|
|
+ var data = { memberId, page, size }
|
|
|
|
|
+ if (category) data.category = category
|
|
|
|
|
+ return request('/api/tasks/history', 'POST', data)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export const deleteTask = (taskId) => {
|
|
export const deleteTask = (taskId) => {
|
|
@@ -1332,18 +1395,11 @@ export const cancelMyJoinRequest = (requestId) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ===== 家庭加入请求 API 别名(供组件统一引用)=====
|
|
// ===== 家庭加入请求 API 别名(供组件统一引用)=====
|
|
|
-export const getPendingFamilyRequests = () => {
|
|
|
|
|
- return request('/api/family/invite/pending-requests', 'POST', {})
|
|
|
|
|
-}
|
|
|
|
|
-export const approveFamilyJoinRequest = (requestId) => {
|
|
|
|
|
- return request('/api/family/invite/approve-request', 'POST', { requestId: requestId })
|
|
|
|
|
-}
|
|
|
|
|
-export const rejectFamilyJoinRequest = (requestId) => {
|
|
|
|
|
- return request('/api/family/invite/reject-request', 'POST', { requestId: requestId })
|
|
|
|
|
-}
|
|
|
|
|
-export const getMyPendingRequest = () => {
|
|
|
|
|
- return request('/api/family/invite/my-request', 'POST', {})
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// @deprecated 别名:委托到上面的规范函数,确保每个 URL 只有一处 request() 调用
|
|
|
|
|
+export const getPendingFamilyRequests = () => getPendingJoinRequests()
|
|
|
|
|
+export const approveFamilyJoinRequest = (requestId) => approveJoinRequest(requestId, '')
|
|
|
|
|
+export const rejectFamilyJoinRequest = (requestId) => rejectJoinRequest(requestId, '')
|
|
|
|
|
+export const getMyPendingRequest = () => getMyJoinRequest()
|
|
|
|
|
|
|
|
// ===== AI 聊天 =====
|
|
// ===== AI 聊天 =====
|
|
|
export const aiSendMessage = (data) => {
|
|
export const aiSendMessage = (data) => {
|
|
@@ -1761,15 +1817,13 @@ export const getWealthDetail = (memberId, memberType) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ===== 成就徽章 =====
|
|
// ===== 成就徽章 =====
|
|
|
-export const getUserBadges = (memberId) => {
|
|
|
|
|
- return request('/api/badges/child/list', 'POST', { memberId })
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// @deprecated 别名:委托到 getChildBadges,确保 /api/badges/child/list 只有一处封装
|
|
|
|
|
+export const getUserBadges = (memberId) => getChildBadges(memberId)
|
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 成长报告 =====
|
|
// ===== 成长报告 =====
|
|
|
-export const getGrowthReport = (memberId) => {
|
|
|
|
|
- return request('/api/stats/child/overview', 'POST', { memberId })
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// @deprecated 别名:委托到 getChildOverview,确保 /api/stats/child/overview 只有一处封装
|
|
|
|
|
+export const getGrowthReport = (memberId) => getChildOverview(memberId)
|
|
|
|
|
|
|
|
// ===== 用户行动统计(我的页面) =====
|
|
// ===== 用户行动统计(我的页面) =====
|
|
|
export const getUserActionStats = () => {
|
|
export const getUserActionStats = () => {
|
|
@@ -2023,9 +2077,8 @@ export const getTodayTasksByCategory = (memberId, category) => {
|
|
|
return request('/api/tasks/today', 'POST', { memberId: memberId, category: category })
|
|
return request('/api/tasks/today', 'POST', { memberId: memberId, category: category })
|
|
|
|
|
|
|
|
}
|
|
}
|
|
|
-export const getTaskHistoryByCategory = (memberId, category, page, size) => {
|
|
|
|
|
- return request('/api/tasks/history', 'POST', { memberId: memberId, category: category, page: page || 1, size: size || 10 })
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// @deprecated 别名:委托到 getTaskHistory(已支持 category 参数),确保 /api/tasks/history 只有一处封装
|
|
|
|
|
+export const getTaskHistoryByCategory = (memberId, category, page, size) => getTaskHistory(memberId, page || 1, size || 10, category)
|
|
|
|
|
|
|
|
// ===== 维度商品(domain 筛选,使用 productList) =====
|
|
// ===== 维度商品(domain 筛选,使用 productList) =====
|
|
|
export const getProductsByDomain = (domain, page, size) => {
|
|
export const getProductsByDomain = (domain, page, size) => {
|