feat(meeting): Task 13 完成 meeting_invite 通知卡片对接

## 后端
- MeetingService.InviteUsers 的 PushPayload.Extra 补齐设计 §10.1 要求字段:
  inviter_id / inviter_name / inviter_avatar / expired_at(Unix 秒,
  与 Redis TTL MeetingInviteTokenTTL=600s 对齐)

## 前端
- constants/notify.js:supportsInlineAction 扩展支持 meeting_invite;
  新增 NOTIFY_INLINE_ACTION_LABEL 映射(meeting_invite → 立即加入/稍后)
  + NOTIFY_INLINE_ACTION_DEFAULT 兜底
- components/notify/NotifyItem.vue:
  * 按 type 动态渲染按钮文案(actionLabel 计算属性)
  * isExpired 计算属性:meeting_invite 比对 extra.expired_at * 1000
  * 过期态合并为单个 disabled 的"邀请已过期"按钮
  * 新增 .notify-btn--expired 灰显样式
- pages/notify/index.vue:
  * handleAccept / handleReject / _navigateByNotify 增加 meeting_invite 分支
  * 新增 _navigateToMeetingInvite 辅助:跳 /pages/meeting/preview?mode=join&code=xxx
  * 过期时 toast "邀请已过期"不跳转,"稍后"仅 markRead 不发接口

## 文档
- docs/plans/2026-04-21-phase2e-2-implementation.plan.md:Task 13 标  + 详细产出
- docs/progress/CURRENT_STATUS.md:新增 Task 13 专节(关键技术点 / 验证记录 / 下一步)
- .cursor/rules/project-context.mdc:2e-2 进度刷 Task 0-13 

## 验证
后端 curl 双角色端到端(testuser1 创会 → 邀请 testuser2 → 拉通知列表):
- type=meeting_invite,extra 8 字段齐全
- expired_at = invited_at + 600,与 Redis TTL 对齐

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-04-22 17:26:31 +08:00
parent 310ea03ce1
commit d6fb7ce291
7 changed files with 193 additions and 41 deletions

View File

@@ -109,7 +109,8 @@ import {
NOTIFY_TYPE_FRIEND_REQUEST,
NOTIFY_TYPE_GROUP_INVITE,
NOTIFY_TYPE_GROUP_JOIN_REQUEST,
NOTIFY_TYPE_GROUP_JOIN_APPROVED
NOTIFY_TYPE_GROUP_JOIN_APPROVED,
NOTIFY_TYPE_MEETING_INVITE
} from '@/constants/notify'
import NotifyItem from '@/components/notify/NotifyItem.vue'
import groupApi from '@/api/group'
@@ -225,7 +226,9 @@ const handleNotifyTap = async (notify) => {
}
/**
* 群邀请/入群申请 —— 内联接受
* 内联"接受"按钮
* - group_invite / group_join_request沿用原逻辑
* - meeting_invite立即加入跳预览页mode=join&code=...
*/
const handleAccept = async (notify) => {
try {
@@ -240,6 +243,11 @@ const handleAccept = async (notify) => {
const extra = _parseExtra(notify.extra)
uni.showToast({ title: '已接受邀请', icon: 'success' })
_navigateToGroup(extra)
} else if (notify.type === NOTIFY_TYPE_MEETING_INVITE) {
const extra = _parseExtra(notify.extra)
if (!_navigateToMeetingInvite(extra)) {
uni.showToast({ title: '邀请信息已失效', icon: 'none' })
}
}
await notifyStore.markRead(notify.id).catch(() => {})
} catch (e) {
@@ -249,7 +257,9 @@ const handleAccept = async (notify) => {
}
/**
* 群邀请/入群申请 —— 内联拒绝
* 内联"拒绝/稍后"按钮
* - group_invite / group_join_request沿用原逻辑
* - meeting_invite仅标记已读不弹 toast"稍后"是轻量操作)
*/
const handleReject = async (notify) => {
try {
@@ -290,6 +300,11 @@ const _navigateByNotify = (notify) => {
case NOTIFY_TYPE_GROUP_JOIN_APPROVED:
_navigateToGroup(extra)
return
case NOTIFY_TYPE_MEETING_INVITE:
if (_navigateToMeetingInvite(extra)) return
// 过期或 extra 缺失时给用户提示
uni.showToast({ title: '邀请已过期或信息缺失', icon: 'none' })
return
default:
break
}
@@ -311,6 +326,25 @@ const _navigateToGroup = (extra) => {
}
}
/**
* 跳转到会议预览页meeting_invite 卡片专用)
*
* 路由协议:/pages/meeting/preview?mode=join&code=xxx
* - 预览页负责让用户确认设备、输入密码(若 has_password再调 joinAndEnter
* - invite_token 当前不在 URL 透传Redis 写入 600sToken 保留冗余兑换
* 后端 RedeemInviteToken 目前在独立接口MVP 阶段仅凭 room_code 加入即可
*
* @returns {boolean} 是否成功发起跳转(用于调用方判断是否需要 fallback 提示)
*/
const _navigateToMeetingInvite = (extra) => {
if (!extra || !extra.room_code) return false
if (extra.expired_at && Number(extra.expired_at) * 1000 < Date.now()) {
return false
}
uni.navigateTo({ url: `/pages/meeting/preview?mode=join&code=${encodeURIComponent(extra.room_code)}` })
return true
}
/** 解析 extra后端以 JSON 字符串存储) */
const _parseExtra = (extra) => {
if (!extra) return null