feat:学生家长导入

feat:增加饭卡列表查询
feat:极光推送过期数据清理
This commit is contained in:
dzc committed 2026-08-13 11:35:25 +08:00
1 parent 6b9c5714dd
commit ea8468e3ef
7 files changed
+200 -117

No files matched your search

+12 -8
View File
@@ -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()
+3 -2
View File
@@ -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" : "用于获取您的位置信息,以支持地图选点、地址定位和相关配送服务。在您使用需要定位的功能时,应用会申请相应定位权限。",
+3 -3
View File
@@ -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)
+23
View File
@@ -201,6 +201,29 @@ export default {
params: { type },
}),
/**
* 将当前 App 安装实例绑定到当前登录用户的极光 alias。
* userId 由后端从 Token 获取,客户端只提交当前设备 registrationId。
*/
bindJgPushDevice: (registrationId: string): Promise<any> =>
request({
url: "/thirdparty/jg-push/device/bind",
method: "POST",
data: { registrationId },
custom: { showError: false },
}),
/**
* 仅解除当前 App 安装实例与当前登录用户极光 alias 的关系。
*/
unbindJgPushDevice: (registrationId: string): Promise<any> =>
request({
url: "/thirdparty/jg-push/device/unbind",
method: "POST",
data: { registrationId },
custom: { showError: false },
}),
// 退出登录(与 PC /auth/token/logout 一致,清除服务端 Token 与 user_details 缓存)
logout: () =>
request({
+103 -32
View File
@@ -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() {
+50 -70
View File
@@ -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 容量、过期设备和精确解绑
- 登录认证与推送同步相互隔离
+6 -2
View File
@@ -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))