feat: 重构 Reminder Notification 系统并更新应用包名

This commit is contained in:
qzl
2026-03-30 18:36:57 +08:00
parent 9fb2a6857b
commit 91bf3c3f96
90 changed files with 5133 additions and 3017 deletions
+59
View File
@@ -104,3 +104,62 @@ Android 安装包命名规范:
- `versionName`(如 `0.1.0`)由开发者手动维护
- `buildNumber`(如 `+2`)由打包脚本自动递增
## 7. Android Production Signing And Upgrade Strategy
为了保证 APK 升级是覆盖安装(保留应用数据和登录态),生产环境必须满足以下条件:
1. `applicationId` 保持不变(当前为 `com.xunmee.xisocial`
2. 所有 `release` 包使用同一套正式签名证书(固定 keystore)
3. `versionCode` 严格递增
若证书变化,Android 通常无法覆盖安装,会要求先卸载旧包再安装新包,导致本地 token/缓存数据丢失。
### 7.1 Signing Files
- 签名配置文件:`apps/android/key.properties`(本地文件,不入库)
- 模板文件:`apps/android/key.properties.example`
- 证书文件建议:`apps/android/release.jks`(本地文件,不入库)
`key.properties` 示例:
```properties
storeFile=release.jks
storePassword=<store_password>
keyAlias=<key_alias>
keyPassword=<key_password>
```
### 7.2 Release Build Contract
- `apps/android/app/build.gradle.kts``release` 构建必须使用 `signingConfigs.release`
-`apps/android/key.properties` 缺失时,构建必须失败(禁止回退到 debug 签名)
### 7.3 Production Packaging Command
```bash
bash deploy/build-android-release.sh \
--backend-host <prod-domain-or-ip> \
--channel release \
--release-notes "<release notes>"
```
产物位置:
- APK`deploy/static/releases/social-app-android-v{versionName}+{versionCode}-release.apk`
- 清单:`deploy/static/releases/manifest.json`
### 7.4 First Migration To Stable Signing
如果历史版本使用 debug 签名,而新版本改为正式签名:
- 首次升级通常需要卸载旧包后安装新包(一次性迁移)
- 迁移完成后,后续版本在同签名条件下可覆盖安装并保留登录态
### 7.5 Operational Checklist (Mandatory)
1. 确认 `applicationId` 未变
2. 确认 `release` 签名证书与上个生产版本一致
3. 确认 `versionCode` 大于上个生产版本
4. 在测试机执行一次覆盖安装验证(旧版登录 -> 升级 -> 登录态保留)
5. 上传新 APK 到 `deploy/static/releases/` 并校验 `manifest.json` 对应条目
@@ -19,6 +19,8 @@
2. 展示文案“取消”必须映射到内部动作 `archive`,并归档对应日程(`status=archived`)。
3. 归档后的 UI 渲染必须灰色显示;不强制改写原始 metadata 颜色。
4. 前端动作上报后端采用最终一致性:本地 outbox + 重放机制。
5. 每轮提醒铃声时长默认 15 秒;若用户未点击通知,系统按 10 分钟节奏进入下一轮提醒。
6. 点击系统通知必须定位到对应日程,并打开提醒详情页(展示日程详情 + 底部动作:归档/稍后提醒)。
---
@@ -55,6 +57,7 @@
- `archive`: 内部归档动作(UI 展示文案为“取消”)
- `snooze10m`: 用户点击稍后提醒,重排到 `now + 10m`
- `defaultRetry10m`: 用户未点击通知时,15 秒铃声结束后自动进入 `now + 10m` 下一轮提醒(内部调度语义,不额外上报动作)
### UI Label Mapping
@@ -105,6 +108,9 @@
### Normal schedule
- `remindAt = startAt - reminderMinutes`
-`remindAt <= now < endAt` 时,启动补偿提醒建议 `now + 5s`
- 每轮提醒后均进入 10 分钟节奏,直到超出 `endAt`
- 截止条件:`fireAt > endAt` 时不再调度后续提醒
### Bootstrap/reinstall compensation
@@ -151,6 +157,7 @@
- 优先 full-screen intent;系统可能因策略降级为 heads-up/横幅。
- 声音和振动受通知通道及系统设置影响。
- 本协议当前实现不依赖 full-screen intent 权限,采用标准高优先级通知 + 点击跳转提醒详情页。
### iOS
@@ -122,6 +122,7 @@ When creating/modifying/deprecating any code, this table must be updated in the
| `SCHEDULE_ITEM_INVITE_ALREADY_SUBSCRIBED` | schedule_items | 400 | Recipient already accepted calendar invite |
| `SCHEDULE_ITEM_INVITE_ALREADY_PENDING` | schedule_items | 400 | Recipient already has pending calendar invite |
| `SCHEDULE_ITEM_AUTH_LOOKUP_UNAVAILABLE` | schedule_items | 503 | Auth/identity lookup unavailable when sharing |
| `SCHEDULE_ITEM_ACTOR_LOOKUP_UNAVAILABLE` | schedule_items | 503 | Actor profile lookup unavailable when constructing inbox change payload |
| `SCHEDULE_ITEM_PENDING_INVITE_NOT_FOUND` | schedule_items | 404 | No pending invitation exists for target item/user |
| `SCHEDULE_ITEM_ACCEPT_SUBSCRIPTION_FAILED` | schedule_items | 503 | Subscription accept flow failed unexpectedly |
| `SCHEDULE_ITEM_REJECT_SUBSCRIPTION_FAILED` | schedule_items | 503 | Subscription reject flow failed unexpectedly |
@@ -129,6 +130,9 @@ When creating/modifying/deprecating any code, this table must be updated in the
| `SCHEDULE_ITEM_DATETIME_REQUIRED` | schedule_items | 400 | Required datetime input missing |
| `INBOX_MESSAGE_NOT_FOUND` | inbox_messages | 404 | Inbox message does not exist for current user |
| `INBOX_MESSAGE_STORE_UNAVAILABLE` | inbox_messages | 503 | Inbox message persistence unavailable |
| `INBOX_SSE_CONNECTION_LIMIT` | inbox_messages | 429 | SSE connections exceed per-user limit |
| `INBOX_INVALID_LAST_EVENT_ID` | inbox_messages | 422 | SSE Last-Event-ID format invalid |
| `INBOX_EVENT_STREAM_UNAVAILABLE` | inbox_messages | 503 | Inbox SSE stream read unavailable |
| `MEMORIES_USER_NOT_FOUND` | memories | 404 | User memory record does not exist |
| `MEMORIES_WORK_NOT_FOUND` | memories | 404 | Work memory record does not exist |
| `MEMORIES_SERVICE_UNAVAILABLE` | memories | 503 | Memories persistence unavailable |
+156 -7
View File
@@ -12,6 +12,7 @@ Base URL: `/api/v1/inbox/messages`
|---|---|---|
| GET | `` | 获取消息列表 |
| PATCH | `/{message_id}/read` | 标记消息为已读 |
| GET | `/stream` | 订阅收件箱实时事件(SSE) |
---
@@ -39,32 +40,80 @@ Base URL: `/api/v1/inbox/messages`
## 消息内容类型
### CalendarInviteContent
### CalendarInviteContent (schema_version=2)
```json
{
"type": "invite",
"schema_version": 2,
"item": {
"id": "uuid",
"title": "string",
"description": "string | null",
"start_at": "datetime",
"end_at": "datetime | null",
"timezone": "string"
},
"actor": {
"user_id": "uuid",
"username": "string",
"phone": "string | null"
},
"summary": "string",
"permission": "int (1=view, 4=edit, 8=invite)",
"action": "pending"
}
```
### CalendarUpdateContent
说明:`description/start_at/end_at/timezone/actor.phone` 为 `invite` 类型的扩展字段,
用于前端展示邀请详情(邀请人、联系电话、时间区间、描述)。
### CalendarUpdateContent (schema_version=2)
```json
{
"type": "update",
"title": "string",
"type": "updated",
"schema_version": 2,
"item": {
"id": "uuid",
"title": "string"
},
"actor": {
"user_id": "uuid",
"username": "string"
},
"summary": "string",
"changes": [
{
"field": "title | description | start_at | end_at | timezone | status",
"label": "string",
"before": "any | null",
"after": "any | null",
"display_before": "string | null",
"display_after": "string | null",
"change_type": "added | removed | modified"
}
],
"action": "updated"
}
```
### CalendarDeleteContent
### CalendarDeleteContent (schema_version=2)
```json
{
"type": "delete",
"title": "string",
"type": "deleted",
"schema_version": 2,
"item": {
"id": "uuid",
"title": "string"
},
"actor": {
"user_id": "uuid",
"username": "string"
},
"summary": "string",
"changes": [],
"action": "deleted"
}
```
@@ -127,3 +176,103 @@ Base URL: `/api/v1/inbox/messages`
### Response
`InboxMessageResponse` 对象。
---
## 3) GET `/stream` (SSE)
订阅当前登录用户的 inbox 实时增量事件。
### Headers
- `Accept: text/event-stream`
- `Last-Event-ID`(可选):断点续流游标,格式 `\d+-\d+`
### Query Parameters
- `idle_limit`: `1..3600`(可选,默认 `300`),连续空轮询上限,超过后服务端主动结束连接。
### SSE 事件帧
```text
id: 1743313300000-0
event: INBOX_MESSAGE_CREATED
data: {"event_id":"6f0d...","occurred_at":"2026-03-30T07:00:00Z","user_id":"...","message_id":"...","op":"created","version":1743313300000,"data":{"message":{...}}}
```
### 事件类型
- `INBOX_MESSAGE_CREATED`
- `INBOX_MESSAGE_READ_CHANGED`
- `INBOX_MESSAGE_STATUS_CHANGED`
- `INBOX_SNAPSHOT_REQUIRED`
### Event Envelope
```json
{
"event_id": "uuid",
"occurred_at": "datetime",
"user_id": "uuid",
"message_id": "uuid",
"op": "created | read_changed | status_changed | snapshot_required",
"version": 1743313300000,
"data": {}
}
```
### Delta 约定
- `created`:
```json
{
"message": {
"id": "uuid",
"recipient_id": "uuid",
"sender_id": "uuid | null",
"message_type": "InboxMessageType",
"schedule_item_id": "uuid | null",
"friendship_id": "uuid | null",
"content": {},
"is_read": false,
"status": "pending",
"created_at": "datetime"
}
}
```
- `read_changed`:
```json
{
"is_read": true
}
```
- `status_changed`:
```json
{
"status": "accepted"
}
```
- `snapshot_required`:
```json
{}
```
### 幂等与补偿策略
- 客户端按 `message_id + version` 做幂等合并;旧版本事件必须丢弃。
- 若检测到版本跳跃或本地状态不可信,客户端应回退到 `GET /api/v1/inbox/messages` 全量快照。
- `id` 字段可用于 `Last-Event-ID` 断点续流。
### 前端渲染约束(强制)
- 对 `message_type=calendar`,前端必须按 `content.type` 严格分发:`invite | updated | deleted`。
- 若 `content` 缺少协议必填字段(`schema_version/item/actor/summary`,以及 `updated` 的 `changes`),前端必须进入协议异常展示路径。
- 禁止将协议异常消息兜底渲染为“默认日历邀请”或其他正常业务消息。