From ea8468e3ef77d3cb6eefcd17db8437ab37a126ff Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Thu, 13 Aug 2026 11:35:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=AD=A6=E7=94=9F=E5=AE=B6=E9=95=BF?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=20feat:=E5=A2=9E=E5=8A=A0=E9=A5=AD=E5=8D=A1?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=9F=A5=E8=AF=A2=20feat:=E6=9E=81=E5=85=89?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E8=BF=87=E6=9C=9F=E6=95=B0=E6=8D=AE=E6=B8=85?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 20 ++-- src/manifest.json | 5 +- src/pages/order/daily-menu-confirm.vue | 6 +- src/sheep/api/user.ts | 23 +++++ src/sheep/stores/user.ts | 135 +++++++++++++++++++------ src/sheep/utils/jg-push/README.md | 120 +++++++++------------- src/sheep/utils/request.ts | 8 +- 7 files changed, 200 insertions(+), 117 deletions(-) diff --git a/src/App.vue b/src/App.vue index 3429d60..5c0e1b3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -19,14 +19,18 @@ export default { this._initTabbar() // #ifdef APP-PLUS this.reportSplashAd() - // 初始化极光推送服务 - await jgPush.initialize({ - debug: true, - autoInitService: true, - enableCrashHandler: true, - requestAndroidPermission: true, - requestIOSNotificationPermission: true, - }) + // 极光属于附属能力,插件或权限初始化失败不能中断 App 启动并造成白屏。 + try { + await jgPush.initialize({ + debug: true, + autoInitService: true, + enableCrashHandler: true, + requestAndroidPermission: true, + requestIOSNotificationPermission: true, + }) + } catch (error) { + console.warn("[JGPush] App 启动时初始化失败,不影响主流程:", error) + } clearAppBadgeNumber() // #endif this._initGlobalWs() diff --git a/src/manifest.json b/src/manifest.json index 444fadd..6df8f41 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,8 +2,8 @@ "name" : "资安信", "appid" : "__UNI__1BFE88F", "description" : "资安信后勤管理系统", - "versionName" : "1.5.13", - "versionCode" : 1513, + "versionName" : "1.5.18", + "versionCode" : 1518, "transformPx" : false, /* 5+App特有相关 */ "app-plus" : { @@ -57,6 +57,7 @@ /* ios打包配置 */ "ios" : { "dSYMs" : false, + "deploymentTarget" : "15.0", "privacyDescription" : { "NSLocationWhenInUseUsageDescription" : "用于获取您的位置信息,以支持地图选点、地址定位和相关配送服务。", "NSLocationAlwaysAndWhenInUseUsageDescription" : "用于获取您的位置信息,以支持地图选点、地址定位和相关配送服务。在您使用需要定位的功能时,应用会申请相应定位权限。", diff --git a/src/pages/order/daily-menu-confirm.vue b/src/pages/order/daily-menu-confirm.vue index c983b8a..6f4fe30 100644 --- a/src/pages/order/daily-menu-confirm.vue +++ b/src/pages/order/daily-menu-confirm.vue @@ -304,9 +304,9 @@ const tabState = reactive({ }) /** 商户是否允许监护人为被监护人代订。 */ -const guardianWardReserveEnabled = computed(() => - (state.mchDetail?.tableReservationConf?.userRelationTypeIdList || []).includes("GUARDIAN_WARD"), -) +const guardianWardReserveEnabled = computed(() => { + return (state.mchDetail?.tableReservationConf?.userRelationTypeIdList || []).includes("GUARDIAN_WARD") +}) /** 当前商户是否限制每个日期餐段的购物车只能选择一份商品。 */ const singleProductOnlyInCart = computed(() => state.mchDetail?.tableReservationConf?.singleProductOnlyInCart === true) diff --git a/src/sheep/api/user.ts b/src/sheep/api/user.ts index a186d73..71b7efe 100644 --- a/src/sheep/api/user.ts +++ b/src/sheep/api/user.ts @@ -201,6 +201,29 @@ export default { params: { type }, }), + /** + * 将当前 App 安装实例绑定到当前登录用户的极光 alias。 + * userId 由后端从 Token 获取,客户端只提交当前设备 registrationId。 + */ + bindJgPushDevice: (registrationId: string): Promise => + request({ + url: "/thirdparty/jg-push/device/bind", + method: "POST", + data: { registrationId }, + custom: { showError: false }, + }), + + /** + * 仅解除当前 App 安装实例与当前登录用户极光 alias 的关系。 + */ + unbindJgPushDevice: (registrationId: string): Promise => + request({ + url: "/thirdparty/jg-push/device/unbind", + method: "POST", + data: { registrationId }, + custom: { showError: false }, + }), + // 退出登录(与 PC /auth/token/logout 一致,清除服务端 Token 与 user_details 缓存) logout: () => request({ diff --git a/src/sheep/stores/user.ts b/src/sheep/stores/user.ts index b87099f..54af25c 100644 --- a/src/sheep/stores/user.ts +++ b/src/sheep/stores/user.ts @@ -187,7 +187,10 @@ export const useUserStore = defineStore("user", { key: "pinia:user", }, actions: { - // 处理登录 + /** + * 完成登录后的用户资料和 TabBar 初始化。 + * 推送同步属于附属能力,任何异常只记录日志,不得改变密码、微信或 Apple 登录结果。 + */ async disposeLogin() { await this._getUserDetail() await this._getUserZsMch() @@ -196,43 +199,111 @@ export const useUserStore = defineStore("user", { tabbarStore?.refreshVisibleTabbar() tabbarStore?.syncByCurrentRoute() // #ifdef APP-PLUS - await jgPush.setAlias(String(this.userId)) - if (this.userInfo) { - const tags = [this.userInfo.deptId].map(String).filter(Boolean) - console.log("更新极光推送api标签:", tags) - jgPush.updateTags(tags) - } + await this._syncJgPushAfterLogin() // #endif }, - // 处理退出登录 + /** + * 正常退出登录。 + * 服务端解绑、本地 alias/标签清理和 Token 注销相互隔离,任一步失败都不能阻止本地退出。 + */ async disposeLogOut() { uni.showLoading({ title: "退出登录中...", mask: true }) try { - await userApi.logout() - } catch (error) { - console.warn("调用退出登录接口失败,仍清除本地登录态:", error) + // #ifdef APP-PLUS + await this._unbindJgPushBeforeLogout() + // #endif + try { + await userApi.logout() + } catch (error) { + console.warn("调用退出登录接口失败,仍清除本地登录态:", error) + } + } finally { + uni.removeStorageSync(STORAGE_KEY.TOKEN) + uni.removeStorageSync(STORAGE_KEY.USERINFO) + this.userInfo = undefined + this.userRoles = [] + this.roleCodes = [] + this.userId = "" + this.tabbarList = [] + this.ZsMch = undefined + this.address = undefined + this.userSelectCity = undefined + this.userSelectCityHistory = [] + uni.hideLoading() + uni.showToast({ title: "退出登录成功", icon: "none" }) + try { + const tabbarStore = await getTabbarStore() + tabbarStore?.setMchFunctionScope({}) + tabbarStore?.reset() + tabbarStore?.hideTabbar() + } catch (error) { + console.warn("退出时重置 TabBar 失败,本地登录态已清除:", error) + } + } + }, + /** + * 登录成功后把当前 registrationId 交给后端管理 alias,并独立更新本机标签。 + * registrationId 尚未生成或任一推送操作失败时只记录警告,不向登录页面抛出。 + */ + async _syncJgPushAfterLogin() { + let registrationId = "" + try { + registrationId = (await jgPush.getRegistrationId()).trim() + if (!registrationId) { + console.warn("[JGPush] 登录后未获取到 registrationId,跳过服务端设备绑定") + } else { + const response: any = await userApi.bindJgPushDevice(registrationId) + if (response?.code !== 0) { + console.warn("[JGPush] 服务端设备绑定失败,不影响登录:", response?.msg || response) + } + } + } catch (error) { + console.warn("[JGPush] 登录后同步设备失败,不影响登录:", error) + } + + if (!this.userInfo) return + try { + const tags = [this.userInfo.deptId].filter(Boolean).map(String) + console.log("更新极光推送api标签:", tags) + await jgPush.updateTags(tags) + } catch (error) { + console.warn("[JGPush] 登录后更新标签失败,不影响登录:", error) + } + }, + /** + * Token 仍有效时先请求后端精确解绑当前设备,再等待本地 alias 和标签清理完成。 + * 三个步骤分别容错,确保远端异常、插件异常或 registrationId 为空均不阻塞退出。 + */ + async _unbindJgPushBeforeLogout() { + const tokenStorage: any = uni.getStorageSync(STORAGE_KEY.TOKEN) + if (!tokenStorage?.access_token) { + console.warn("[JGPush] 退出时 Token 已不存在,跳过服务端解绑") + } else { + try { + const registrationId = (await jgPush.getRegistrationId()).trim() + if (!registrationId) { + console.warn("[JGPush] 退出时未获取到 registrationId,跳过服务端解绑") + } else { + const response: any = await userApi.unbindJgPushDevice(registrationId) + if (response?.code !== 0) { + console.warn("[JGPush] 服务端设备解绑失败,继续退出:", response?.msg || response) + } + } + } catch (error) { + console.warn("[JGPush] 服务端设备解绑异常,继续退出:", error) + } + } + + try { + await jgPush.deleteAlias() + } catch (error) { + console.warn("[JGPush] 本地 alias 清理失败,继续退出:", error) + } + try { + await jgPush.cleanTags() + } catch (error) { + console.warn("[JGPush] 本地标签清理失败,继续退出:", error) } - uni.removeStorageSync(STORAGE_KEY.TOKEN) - uni.removeStorageSync(STORAGE_KEY.USERINFO) - this.userInfo = undefined - this.userRoles = [] - this.roleCodes = [] - this.userId = "" - this.tabbarList = [] - this.ZsMch = undefined - this.address = undefined - this.userSelectCity = undefined - this.userSelectCityHistory = [] - uni.hideLoading() - // #ifdef APP-PLUS - jgPush.deleteAlias() - jgPush.cleanTags() - // #endif - uni.showToast({ title: "退出登录成功", icon: "none" }) - const tabbarStore = await getTabbarStore() - tabbarStore?.setMchFunctionScope({}) - tabbarStore?.reset() - tabbarStore?.hideTabbar() }, // 获取用户详情 async _getUserDetail() { diff --git a/src/sheep/utils/jg-push/README.md b/src/sheep/utils/jg-push/README.md index 2492ca9..1acbd49 100644 --- a/src/sheep/utils/jg-push/README.md +++ b/src/sheep/utils/jg-push/README.md @@ -124,9 +124,9 @@ console.log(pushStore.notificationPermission) 推荐的接入顺序如下: 1. 应用启动时初始化推送 -2. 登录成功后绑定别名 +2. 登录成功后获取 `registrationId` 并提交后端绑定 3. 需要时监听通知事件 -4. 退出登录时解绑别名并重置运行态 +4. 退出登录时先请求后端精确解绑,再清理本地 alias 和标签 ### 1. 应用启动初始化 @@ -168,66 +168,53 @@ export default { - `requestIOSNotificationPermission`:iOS 是否主动查询或申请通知授权 - `iosForegroundOptions`:iOS 前台通知展示策略 -### 2. 登录成功后绑定别名 +### 2. 登录成功后绑定设备 -当前项目推荐将业务 `userId` 绑定为极光 `alias`。 +当前项目由后端将业务 `userId` 作为极光 `alias`,App 不再直接调用 `jgPush.setAlias()`。 原因: -- 一个用户对应一个稳定推送身份 -- 服务端按用户维度推送更直接 -- 与项目已有约定保持一致 +- 后端可以在绑定前统一清理过期和超量设备 +- `userId` 从登录 Token 获取,客户端不能绑定其他用户 +- 密码、微信和 Apple 登录共用同一条推送同步路径 +- 推送失败属于附属能力失败,不会改变登录认证结果 -示例接入点可放在登录成功后,例如当前项目的 [login.vue](file:///Users/dengzhangchen/Desktop/work/zax/hq-platform/biz_zax_app_cloud/src/pages/root/login.vue#L60-L97) 登录成功后。 +绑定逻辑已统一放在 `src/sheep/stores/user.ts` 的 `disposeLogin()` 中。各登录页面只需在保存 Token 后调用该方法,不要再追加别名操作。 示例: ```ts -import jgPush from "@@/utils/jg-push" import { useUserStore } from "@@/stores/user" const userStore = useUserStore() const onLoginSuccess = async () => { + // disposeLogin 内部会获取 registrationId 并调用后端绑定接口;推送失败只记录警告。 await userStore.disposeLogin() - - const userId = userStore.userId - if (!userId) return - - try { - await jgPush.initialize({ - debug: import.meta.env.DEV, - autoInitService: true, - }) - await jgPush.setAlias(String(userId)) - } catch (error) { - console.error("[JPush] 登录后绑定别名失败", error) - } } ``` ### 3. 退出登录时解绑 -推荐在退出登录时做以下动作: +`disposeLogOut()` 按以下顺序执行: -1. 删除极光别名 -2. 清空或重置推送运行态 +1. Token 有效时获取当前 `registrationId` +2. 调用后端接口精确解绑当前设备 +3. 调用 `jgPush.deleteAlias()` 作为本地兜底 +4. 调用 `jgPush.cleanTags()` 清理本地标签 +5. 调用服务端 Token 注销接口 +6. 在 `finally` 中清除本地登录态 -当前项目可考虑放在 [user.ts](file:///Users/dengzhangchen/Desktop/work/zax/hq-platform/biz_zax_app_cloud/src/sheep/stores/user.ts#L199-L223) 的 `disposeLogOut()` 中。 +上述步骤相互隔离,任何一步失败都不能阻止本地退出。强杀 App 或系统回收进程时无法保证执行解绑,后端会通过过期设备策略兜底。 示例: ```ts -import jgPush from "@@/utils/jg-push" +import { useUserStore } from "@@/stores/user" async function handleLogout() { - try { - await jgPush.deleteAlias() - } catch (error) { - console.warn("[JPush] 删除别名失败", error) - } finally { - jgPush.reset() - } + const userStore = useUserStore() + await userStore.disposeLogOut() } ``` @@ -419,13 +406,17 @@ jgPush.clearListeners("notification") ## 别名与标签管理 -### 设置别名 +### 底层设置别名 + +`setAlias()` 仅作为原生插件底层能力保留,当前项目的登录流程禁止直接调用。业务登录必须提交 `registrationId` 给后端统一绑定。 ```ts await jgPush.setAlias("10001") ``` -### 删除别名 +### 本地删除别名 + +`deleteAlias()` 只用于退出登录时的本地兜底,不能替代后端精确解绑接口。 ```ts await jgPush.deleteAlias() @@ -721,7 +712,7 @@ await jgPush.deleteGeofence("beijing") import jgPush, { PushServiceError } from "@@/utils/jg-push" try { - await jgPush.setAlias("10001") + await jgPush.updateTags(["canteen"]) } catch (error) { if (error instanceof PushServiceError) { console.error(error.code, error.message, error.detail) @@ -740,13 +731,13 @@ try { - `PUSH_CALLBACK_TIMEOUT` - `PUSH_NATIVE_ERROR` -## 与当前项目结合的推荐接入方案 +## 与当前项目结合的接入方案 -结合当前项目现状,建议这样落地: +当前项目已经按以下方式接入: ### App 启动时初始化 -可放在 [App.vue](file:///Users/dengzhangchen/Desktop/work/zax/hq-platform/biz_zax_app_cloud/src/App.vue#L16-L27) 的 `onLaunch()` 内。 +初始化位于 `src/App.vue` 的 `onLaunch()` 内,并使用独立异常捕获,插件或权限初始化失败不会阻断 App 启动。 建议写法: @@ -776,38 +767,30 @@ onLaunch() { } ``` -### 登录成功后绑定别名 +### 登录成功后同步设备 -当前登录成功后的代码在 [login.vue](file:///Users/dengzhangchen/Desktop/work/zax/hq-platform/biz_zax_app_cloud/src/pages/root/login.vue#L84-L95)。 +密码、微信和 Apple 登录成功后都会调用 `userStore.disposeLogin()`。该方法获取当前 `registrationId`,调用 `/thirdparty/jg-push/device/bind`,然后独立更新标签。 -建议在 `await userStore.disposeLogin()` 后加: +登录调用方只保留: ```ts -try { - if (userStore.userId) { - await jgPush.setAlias(String(userStore.userId)) - } -} catch (error) { - console.warn("[JPush] setAlias failed", error) -} +await userStore.disposeLogin() ``` -### 退出登录时解绑并重置 +后端绑定或标签更新失败只记录警告,不向登录页面抛出,也不触发统一错误 Toast。 -当前退出逻辑在 [user.ts](file:///Users/dengzhangchen/Desktop/work/zax/hq-platform/biz_zax_app_cloud/src/sheep/stores/user.ts#L199-L223)。 +### 退出登录时精确解绑并清理本地状态 -建议增加: +退出逻辑统一位于 `src/sheep/stores/user.ts` 的 `disposeLogOut()`。 + +页面只需调用: ```ts -try { - await jgPush.deleteAlias() -} catch (error) { - console.warn("[JPush] deleteAlias failed", error) -} finally { - jgPush.reset() -} +await userStore.disposeLogOut() ``` +App 原生推送逻辑均位于 `APP-PLUS` 条件编译分支,H5 不调用极光原生插件。 + ## 待跳转路由说明 如果通知点击事件的 `extras` 中带有以下字段: @@ -892,13 +875,13 @@ console.log(pushStore) console.log(jgPush.getStatus()) ``` -## 建议的业务接入优先级 +## 业务接入顺序 -如果你准备在当前项目正式接入,建议按这个顺序来: +新增登录方式或推送业务时按这个顺序接入: -1. 在 `App.vue` 完成初始化 -2. 在登录成功后绑定 `alias` -3. 在退出登录时解绑并 `reset` +1. 在 `App.vue` 初始化推送,并独立捕获初始化异常 +2. 登录成功后统一调用 `userStore.disposeLogin()`,由后端绑定设备 +3. 退出登录统一调用 `userStore.disposeLogOut()`,由后端精确解绑当前设备 4. 在首页或业务层加通知事件监听 5. 根据业务需要再补 `tags`、本地通知、跳转队列处理 @@ -913,8 +896,5 @@ console.log(jgPush.getStatus()) - 别名、标签、角标、本地通知、权限、地理能力完整封装 - 错误统一包装 - 与当前项目的登录态和全局状态体系兼容 - -如果你需要,我下一步可以继续帮你做两件事中的任意一个: - -1. 直接把这份文档里的接入代码真正落到 `App.vue`、`login.vue`、`user.ts` -2. 再补一份“服务端推送字段约定文档”,明确 `extras` 里该怎么传 `path/query` +- 后端统一管理 alias 容量、过期设备和精确解绑 +- 登录认证与推送同步相互隔离 diff --git a/src/sheep/utils/request.ts b/src/sheep/utils/request.ts index e7f0e68..5273a93 100644 --- a/src/sheep/utils/request.ts +++ b/src/sheep/utils/request.ts @@ -41,6 +41,7 @@ const SKIP_ENC_URLS = [ "/third/third-app-oath/", "/admin/user/forgot-password", "/admin/register/default-user", + "/admin/social/apple/resolve", "/auth/token/logout", ] @@ -138,16 +139,19 @@ https.interceptors.response.use( return Promise.resolve(unwrapEncryptedResponse(response.data)) }, async (error) => { + const shouldShowError = error.config?.custom?.showError !== false // 处理 424 错误(token过期) if (error.statusCode === 424) { // 开发环境直接清除token并跳转登录 uni.removeStorageSync(TOKEN_KEY) uni.removeStorageSync(USER_INFO_KEY) - uni.showToast({ title: "登录已过期,请重新登录", icon: "none" }) + if (shouldShowError) { + uni.showToast({ title: "登录已过期,请重新登录", icon: "none" }) + } router.jump("/pages/root/login", {}, 2) } // 登录错误由登录页自行处理 - else if (error?.data?.msg && error.config?.url !== "/auth/oauth2/token") { + else if (error?.data?.msg && error.config?.url !== "/auth/oauth2/token" && shouldShowError) { uni.showToast({ title: error.data.msg, icon: "none", mask: true }) } return Promise.resolve(unwrapEncryptedResponse(error.data))