Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
CFFP-HB
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Chao Sun
CFFP-HB
Commits
47f57b61
Commit
47f57b61
authored
Nov 10, 2026
by
yuzhenWang
Browse files
Options
Browse Files
Download
Plain Diff
合并代码解决冲突
parents
22e6be98
fd04b50f
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
203 additions
and
56 deletions
+203
-56
pages/courselist/courselist.vue
+1
-1
pages/index/index.vue
+23
-22
util/fiveshare.ts
+27
-2
util/interceptor.ts
+152
-31
No files found.
pages/courselist/courselist.vue
View file @
47f57b61
...
...
@@ -558,7 +558,7 @@
queryAreaCenterInfo
()
{
api
.
queryAreaCenterInfo
({
userId
:
uni
.
getStorageSync
(
'cffp_userId'
),
//
userId: uni.getStorageSync('cffp_userId'),
companyType
:
`
${
companyInfo
.
companyType
}
`
}).
then
((
res
)
=>
{
if
(
res
[
'success'
])
{
...
...
pages/index/index.vue
View file @
47f57b61
...
...
@@ -531,27 +531,28 @@
]
uni
.
removeTabBarBadge
({
index
:
3
});
api
.
loginVerification
({
"loginType"
:
1
,
}).
then
((
res
)
=>
{
if
(
res
[
'success'
])
{
this
.
userId
=
String
(
res
[
'data'
][
'userId'
]);
uni
.
setStorageSync
(
'isLogin'
,
'1'
);
uni
.
setStorageSync
(
'cffp_userId'
,
this
.
userId
);
uni
.
setStorageSync
(
'loginType'
,
'visitor'
);
uni
.
setStorageSync
(
'uni-token'
,
res
.
data
[
'token'
]);
this
.
courseList
();
this
.
getOneProduct
()
this
.
initShare
();
this
.
queryAreaCenterInfo
()
}
else
{
uni
.
showToast
({
title
:
res
[
'message'
],
duration
:
2000
,
icon
:
'none'
})
}
})
// api.loginVerification({
// "loginType": 1,
// }).then((res) => {
// if (res['success']) {
// this.userId = String(res['data']['userId']);
// uni.setStorageSync('isLogin', '1');
// uni.setStorageSync('cffp_userId', this.userId);
// uni.setStorageSync('loginType', 'visitor');
// uni.setStorageSync('uni-token', res.data['token']);
// } else {
// uni.showToast({
// title: res['message'],
// duration: 2000,
// icon: 'none'
// })
// }
// })
this
.
courseList
();
this
.
getOneProduct
()
this
.
initShare
();
this
.
queryAreaCenterInfo
()
}
},
querySystemMessage
()
{
...
...
@@ -737,7 +738,7 @@
// 获取轮播图
queryAreaCenterInfo
()
{
api
.
queryAreaCenterInfo
({
userId
:
uni
.
getStorageSync
(
'cffp_userId'
),
//
userId: uni.getStorageSync('cffp_userId'),
companyType
:
`
${
companyInfo
.
companyType
}
`
}).
then
((
res
)
=>
{
if
(
res
[
'success'
])
{
...
...
util/fiveshare.ts
View file @
47f57b61
...
...
@@ -66,6 +66,27 @@ function ensureWechatSDK(): Promise<void> {
});
}
// ===== 排查分享问题的调试开关 =====
// 改为 true 后 wx.config 会弹窗显示签名结果与每个 jsApi 权限,
// 签名不对会明确提示 invalid signature(URL 不一致)或 invalid domain(安全域名未配置)
const
WX_SHARE_DEBUG
=
false
// ===== 签名 URL(分享能不能生效的关键) =====
// 本项目 H5 是 history 路由,页面跳转后 window.location.href 会变。
// iOS 微信校验签名用的是「首次进入页面那一刻的 URL」,Android 微信用「当前 URL」。
// 所以 iOS 必须用 main.js 存下的 firstEntryUrl(首次进入 URL),否则必定 invalid signature。
function
getSignUrl
(
fallbackUrl
:
string
):
string
{
const
isIOS
=
/iphone|ipad|ipod/i
.
test
(
navigator
.
userAgent
)
if
(
isIOS
&&
typeof
window
!==
'undefined'
&&
window
.
sessionStorage
)
{
const
firstUrl
=
window
.
sessionStorage
.
getItem
(
'firstEntryUrl'
)
if
(
firstUrl
)
{
console
.
log
(
'[fiveshare] iOS 使用首次进入 URL 签名:'
,
firstUrl
);
return
firstUrl
}
}
return
fallbackUrl
}
// ===== 初始化微信 SDK =====
export
function
initJssdkShare
(
callback
:
any
,
url
:
string
)
{
// 只在微信浏览器且非小程序环境中初始化
...
...
@@ -89,14 +110,18 @@ export function initJssdkShare(callback: any, url: string) {
return
;
}
const
rawUrl
=
url
||
window
.
location
.
href
.
split
(
'#'
)[
0
];
const
signUrl
=
getSignUrl
(
rawUrl
);
console
.
log
(
'[fiveshare] 当前 URL:'
,
window
.
location
.
href
.
split
(
'#'
)[
0
],
'| 签名 URL:'
,
signUrl
);
const
WxConfigRequestVO
=
{
url
:
url
||
window
.
location
.
href
.
split
(
'#'
)[
0
]
,
url
:
signUrl
,
systemType
:
uni
.
getStorageSync
(
'addSystemType'
)
||
'1'
};
api
.
Wxshare
(
WxConfigRequestVO
).
then
(
res
=>
{
wx
.
config
({
debug
:
false
,
debug
:
WX_SHARE_DEBUG
,
appId
:
res
.
data
.
appId
,
timestamp
:
res
.
data
.
timestamp
,
nonceStr
:
res
.
data
.
nonceStr
,
...
...
util/interceptor.ts
View file @
47f57b61
...
...
@@ -113,6 +113,145 @@ const whiteApiList = [`${apiURL}/authorize/obtainToken`,
`
${
cffpURL
}
/partner/queryById`
,
`
${
apiURL
}
/sso/ticket/consume`
,
// Casdoor 票据消费:匿名,唯一凭据是 ticket
];
// ==================== token 获取 & 整页刷新 控制 ====================
// obtainToken + 整页刷新是兼容 iOS 微信分享的既有方案(先让 token 就位,再渲染页面)。
// 但首页接口(如 queryAreaCenterInfo)不在白名单又需要 token,会一运行就无限刷新,这里补 4 道保护:
// 1. 单飞:同一时刻只发一个 obtainToken,避免并发请求各自刷新
// 2. 校验:没拿到有效 token 一律不写、不刷新(防写入 undefined 后反复刷新)
// 3. 上限:时间窗内最多刷新 MAX_RELOAD 次,超出只补 token、不再刷新
// 4. 不覆盖:页面自己(loginVerification)已拿到 token 时不再写入、不再刷新
const
TOKEN_KEY
=
'uni-token'
const
RELOAD_COUNT_KEY
=
'__cffp_token_reload_count'
const
RELOAD_TS_KEY
=
'__cffp_token_reload_ts'
const
MAX_RELOAD
=
3
const
MIN_RELOAD_INTERVAL
=
800
const
RELOAD_RESET_WINDOW
=
30000
let
obtaining
=
false
let
t001Locked
=
false
const
hasWindow
=
typeof
window
!==
'undefined'
const
canUseSession
=
hasWindow
&&
!!
window
.
sessionStorage
// 读取有效 token(兼容历史脏数据 'undefined' / 'null')
const
readToken
=
()
=>
{
const
token
=
uni
.
getStorageSync
(
TOKEN_KEY
)
if
(
!
token
||
token
===
'undefined'
||
token
===
'null'
)
return
''
return
token
}
const
isWhiteApi
=
(
url
=
''
)
=>
whiteApiList
.
some
(
item
=>
url
===
item
||
url
.
indexOf
(
item
)
===
0
)
const
readReloadState
=
()
=>
{
let
count
=
0
let
ts
=
0
if
(
canUseSession
)
{
count
=
Number
(
window
.
sessionStorage
.
getItem
(
RELOAD_COUNT_KEY
)
||
0
)
ts
=
Number
(
window
.
sessionStorage
.
getItem
(
RELOAD_TS_KEY
)
||
0
)
}
// 超过窗口期没再刷新,说明循环已断开,计数归零
if
(
!
ts
||
Date
.
now
()
-
ts
>
RELOAD_RESET_WINDOW
)
count
=
0
return
{
count
,
ts
}
}
const
canReload
=
()
=>
{
if
(
!
hasWindow
)
return
false
const
{
count
,
ts
}
=
readReloadState
()
if
(
count
>=
MAX_RELOAD
)
return
false
if
(
ts
&&
Date
.
now
()
-
ts
<
MIN_RELOAD_INTERVAL
)
return
false
return
true
}
// 去掉 URL 上一次的 t_reload,避免反复刷新时参数无限堆积
const
stripReloadParam
=
(
url
)
=>
{
if
(
url
.
indexOf
(
't_reload='
)
===
-
1
)
return
url
const
index
=
url
.
indexOf
(
'?'
)
if
(
index
===
-
1
)
return
url
const
path
=
url
.
slice
(
0
,
index
)
const
kept
=
url
.
slice
(
index
+
1
).
split
(
'&'
).
filter
(
kv
=>
kv
&&
kv
.
indexOf
(
't_reload='
)
!==
0
)
return
kept
.
length
?
`
${
path
}
?
${
kept
.
join
(
'&'
)}
`
:
path
}
// 整页刷新:参数必须放在 hash 之前,只有 URL 主体变化浏览器才会重新加载文档
// (hash 内加参数不会触发整页刷新,这也是原写法在带参页面上刷新不生效的原因)
const
reloadPage
=
()
=>
{
if
(
!
hasWindow
)
return
const
{
count
}
=
readReloadState
()
if
(
canUseSession
)
{
window
.
sessionStorage
.
setItem
(
RELOAD_COUNT_KEY
,
String
(
count
+
1
))
window
.
sessionStorage
.
setItem
(
RELOAD_TS_KEY
,
String
(
Date
.
now
()))
}
const
href
=
window
.
location
.
href
const
hashIndex
=
href
.
indexOf
(
'#'
)
let
base
=
stripReloadParam
(
hashIndex
>
-
1
?
href
.
slice
(
0
,
hashIndex
)
:
href
)
const
hash
=
hashIndex
>
-
1
?
href
.
slice
(
hashIndex
)
:
''
base
+=
(
base
.
indexOf
(
'?'
)
>
-
1
?
'&'
:
'?'
)
+
't_reload='
+
Date
.
now
()
// 用 replace 而非直接赋值 href,避免浏览器历史里堆一长串刷新记录
window
.
location
.
replace
(
base
+
hash
)
}
const
obtainTokenOnce
=
()
=>
{
if
(
obtaining
)
return
obtaining
=
true
const
params
:
any
=
{
ticket
:
'uni-app'
,
loginId
:
null
}
const
h5userId
=
uni
.
getStorageSync
(
'cffp_userId'
)
if
(
h5userId
)
params
.
loginId
=
h5userId
uni
.
request
({
url
:
`
${
apiURL
}
/authorize/obtainToken`
,
method
:
'POST'
,
data
:
params
,
success
:
(
res
:
any
)
=>
{
const
body
=
res
&&
res
.
data
const
token
=
body
&&
body
.
data
&&
body
.
data
.
token
?
body
.
data
.
token
:
(
body
&&
body
.
token
)
if
(
res
.
statusCode
!==
200
||
!
token
)
{
console
.
warn
(
'[interceptor] obtainToken 未拿到有效 token,跳过写入与刷新'
,
res
)
return
}
// 这里必须无条件刷新一次:本次是为"已经裸奔发出的无 token 请求"补的 token,
// 不重载的话那个请求会带着空 X-Authorization 一直失败,看起来就是"有 token 但请求没带"
const
existed
=
readToken
()
if
(
!
existed
)
uni
.
setStorageSync
(
TOKEN_KEY
,
token
)
if
(
canReload
())
{
reloadPage
()
}
else
{
console
.
warn
(
'[interceptor] token 已补齐,但刷新次数已达上限,不再整页刷新'
)
}
},
fail
:
(
err
:
any
)
=>
{
console
.
warn
(
'[interceptor] obtainToken 请求失败,跳过刷新'
,
err
)
},
complete
:
()
=>
{
obtaining
=
false
}
})
}
// T001 = token 失效 / 未登录
const
handleTokenInvalid
=
()
=>
{
if
(
t001Locked
)
return
const
loginType
=
uni
.
getStorageSync
(
'loginType'
)
// 访客态 token 由 obtainToken 自动续期,这里清 token 会和请求拦截器互相触发,形成刷新死循环
if
(
loginType
===
'visitor'
)
{
console
.
warn
(
'[interceptor] 访客态接口返回 T001,跳过清理(避免触发整页刷新循环)'
)
return
}
t001Locked
=
true
uni
.
removeStorageSync
(
'isLogin'
)
uni
.
removeStorageSync
(
TOKEN_KEY
)
uni
.
setStorageSync
(
'loginType'
,
'visitor'
)
const
pages
=
typeof
getCurrentPages
===
'function'
?
getCurrentPages
()
:
[]
const
current
=
pages
.
length
?
(
pages
[
pages
.
length
-
1
]
as
any
).
route
:
''
if
(
current
!==
'pages/index/index'
)
{
uni
.
switchTab
({
url
:
'/pages/index/index'
})
}
// 短时间内多个请求同时返回 T001,只处理一次
setTimeout
(()
=>
{
t001Locked
=
false
},
500
)
}
export
const
interceptor
=
()
=>
{
uni
.
addInterceptor
(
'request'
,
{
// 请求拦截器
...
...
@@ -147,35 +286,20 @@ export const interceptor = () => {
return
}
// 当本地没有token,并且接口地址没在白名单内,需要重新获取token
if
(
!
uni
.
getStorageSync
(
'uni-token'
)
&&
!
whiteApiList
.
includes
(
args
.
url
))
{
const
params
=
{
ticket
:
'uni-app'
,
loginId
:
null
}
let
h5userId
=
uni
.
getStorageSync
(
'cffp_userId'
);
if
(
h5userId
)
{
params
.
loginId
=
h5userId
;
// 注意:并发请求只会触发一次(单飞),且刷新有次数上限,不会无限刷新
if
(
!
readToken
()
&&
!
isWhiteApi
(
args
.
url
))
{
// 只有确定会整页重载时才取消本请求,避免"带着空 token 裸奔"打到后端被 T001
const
willReload
=
hasWindow
&&
canReload
()
obtainTokenOnce
()
if
(
willReload
)
{
// 取消本次请求,等整页重载后由页面用已就位的 token 重新发起
return
false
}
uni
.
request
({
url
:
`
${
apiURL
}
/authorize/obtainToken`
,
method
:
'POST'
,
data
:
params
,
success
:
(
res
)
=>
{
if
(
res
.
statusCode
===
200
)
{
uni
.
setStorageSync
(
'uni-token'
,
res
.
data
[
'data'
][
'token'
]);
let
isHas
=
window
.
location
.
href
.
indexOf
(
'?'
)
==-
1
?
'?'
:
'&'
;
window
.
location
.
href
=
window
.
location
.
href
+
isHas
+
't_reload='
+
new
Date
().
getTime
();
}
}
})
}
//设置请求头及token
args
.
header
=
{
'content-type'
:
args
.
method
===
'POST'
?
'application/json'
:
'application/x-www-form-urlencoded'
,
'X-Authorization'
:
uni
.
getStorageSync
(
'uni-token'
)
?
uni
.
getStorageSync
(
'uni-token'
)
:
''
,
'X-Authorization'
:
readToken
()
,
// 'Access-Control-Allow-Headers': 'appId',
// 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
// 'Access-Control-Max-Age': 86400,
...
...
@@ -191,18 +315,15 @@ export const interceptor = () => {
const
unauthorized
=
sso
.
isSsoMode
()
?
args
.
statusCode
===
401
:
bodyT001
if
(
unauthorized
)
{
if
(
unauthorized
)
{
if
(
sso
.
isSsoMode
())
{
// 不回 H5 首页/登录页:销毁 web-view,原生强制重走 wx.login→票据
sso
.
reloginViaMiniprogram
()
return
}
uni
.
removeStorageSync
(
'isLogin'
);
uni
.
removeStorageSync
(
'uni-token'
);
uni
.
setStorageSync
(
'loginType'
,
'visitor'
)
uni
.
switchTab
({
url
:
'/pages/index/index'
})
// 非 sso 模式统一走 handleTokenInvalid:访客态跳过清理、短时间内并发 T001 只处理一次,
// 避免与请求拦截器的 obtainToken + 整页刷新互相触发形成刷新死循环
handleTokenInvalid
()
}
},
fail
()
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment