# 城市提示与实时位置共享

城市贴纸推荐、一次性地点消息和实时位置共享是不同功能。城市识别只为推荐素材；实时共享会把精确坐标和时间持续写入聊天服务，必须由用户显式开始，并在停止、权限变化、断网和退出账号时分别处理本机采集与远端记录。

## 三条位置路径

| 路径 | 数据与用途 | 是否自动开始实时共享 |
| --- | --- | --- |
| 城市提示`GET /location/city` | 请求来源公网地址映射到city、countryCode、source=ip；可返回null | 否 |
| 城市贴纸设备定位 | 前台取一次定位并经系统地理编码得到城市，再匹配本地目录 | 否 |
| `/conversations/{id}/live-locations`或`/groups/{id}/live-locations` | 当前范围用户的经纬度、精度、开始和更新时间 | 只有POST显式开始 |

城市接口不返回街道或坐标。服务端仅在连接对端属于可信代理allowlist时接受代理覆盖的单个X-Real-IP，不读取X-Forwarded-For；非公网来源、目录缺失、校验失败或查不到城市时返回null而非伪造结果。依据：`chat-service/src/city-location.mjs:99-124`。

iOS城市定位期望约三公里精度、10秒取值超时，不启用后台更新；iOS选择器并行尝试设备与IP，设备结果优先，10分钟内可复用结果，手动选择会使旧自动任务失效。Android城市定位只做一次前台fix，地理编码前把经纬度量化到0.02度网格。系统地理编码仍有其系统服务的数据边界，不应把“坐标不传聊天城市接口”扩大成“整个定位过程不接触外部服务”。依据：`ios-app/Sources/Features/Chat/CityStickerDeviceLocator.swift:18-108`、`ios-app/Sources/Features/Chat/CityStickerSelection.swift:36-113`、`android-app/app/src/main/java/cn/thestarline/workbench/CityStickerLocation.kt:44-90`。

## 实时共享的数据与权限

| 字段 | 类型与约束 |
| --- | --- |
| scopeKind、scopeId | conversation/group与对应聊天ID；用不同路径区分 |
| userId、displayName | 由已登录会话决定；请求不能替别人上传 |
| latitude、longitude | 有限数字；纬度-90至90，经度-180至180 |
| accuracy | 有限数字，0–100000，单位米；不是“误差越大越可信” |
| startedAt、updatedAt | 服务端Unix毫秒；前者首次开始时间，后者最近成功写入时间 |

所有操作都要求Bearer与当前范围成员资格。单聊POST/PUT还要求仍是好友且任一方均未屏蔽；DELETE只停止自己的共享。群GET/POST/PUT/DELETE要求当前成员，不要求每两位群成员互为好友。服务端存储可读坐标，不是MLS密文；同范围有权成员能读取全部当前共享者。

依据：`chat-service/src/server.mjs:101-109,582-610`、`chat-service/src/database.mjs:1037-1085`。

## 从开始到恢复的操作次序

1. 用户明确选择共享并确认，客户端请求系统位置权限；Android当前界面还要求通知权限，用于持续共享通知。
2. 获得有效坐标后POST范围路径，请求只包含latitude、longitude、accuracy。成功201 `{location}`后才登记本机共享范围并启动持续定位。
3. 后续位置更新使用PUT，同一用户、范围只更新一条记录；首次startedAt不重置，updatedAt更新为本次服务端时间。
4. 页面单独GET范围位置，当前两端聊天面板约每4秒刷新；位置写入没有普通/events通知，不应等待message事件才刷新地图。
5. 登录或恢复时GET `/live-locations/mine`取得本人的共享范围，然后与权限和本机定位状态协调。
6. 用户结束该范围时DELETE，成功200 `{status:"stopped"}`后从本机集合和界面移除；没有其他共享范围才停止持续采集。

POST会upsert既有自己的记录，PUT不会创建新共享：未POST就PUT返回409 NOT_SHARING。客户端不能在PUT失败时自动POST恢复，因为这可能在用户已结束共享后重新开启。依据：`chat-service/src/database.mjs:1063-1085`、`ios-app/Sources/Features/Chat/ChatStore.swift:577-645`。

合成正常请求：`{"latitude":0,"longitude":0,"accuracy":25}`是合法测试坐标值，不代表用户真实位置。POST后GET应出现当前用户并有两个时间字段；DELETE后GET不再出现。合成失败请求：`{"latitude":91,"longitude":0,"accuracy":25}`返回400 INVALID_LOCATION；不应因缺少定位结果而把用户默认为零坐标发送。

同一个账号可同时向多个聊天范围共享，同一条本机定位会依次PUT到ownLiveScopes中的各范围。停止一个聊天不会停止其他范围；只有本机剩余范围为空才释放持续监听。确认“全部结束”时应重新读取mine，而不是只看当前聊天面板为空。范围内没有自己的记录，也不表示同账号在另一个群没有共享。依据：`ios-app/Sources/Features/Chat/ChatStore.swift:607-645`、`android-app/app/src/main/java/cn/thestarline/workbench/AppState.kt:387-449`。

## 持续定位的双端实现

| 场景 | iOS | Android |
| --- | --- | --- |
| 采集设置 | best accuracy、distanceFilter=10米，禁止系统自动暂停，允许后台更新 | GPS/NETWORK provider更新，最小间隔5秒、距离10米，前台定位服务 |
| 上传节流 | 至少间隔5秒，同一时刻仅一轮上传 | 单调时钟间隔至少5秒、AtomicBoolean避免并行上传 |
| 过旧fix | 接收精度非负且时间不旧于约60秒的定位 | 精度0–100000、时间不旧于60秒 |
| 范围恢复 | 按mine重建ownLiveScopes | 服务约30秒同步范围，持续通知显示状态 |
| 普通页面停止 | 先等DELETE成功，再改本机范围集合 | 同样先等DELETE成功，再停止相应本机状态 |

这些是程序配置与调度目标，不保证操作系统每5秒交付一次定位，也不代表锁屏、后台权限和省电模式已完成设备验收。iOS仅在至少一个共享范围存在时持续更新；Android服务返回START_NOT_STICKY，不保证被系统终止后自动复活。依据：`ios-app/Sources/Features/Chat/ChatLiveLocation.swift:41-107`、`android-app/app/src/main/java/cn/thestarline/workbench/LiveLocationService.kt:54-171`。

## 时间、可信程度与页面一致性

updatedAt由服务器在成功接收POST/PUT时生成，协议没有单独的定位fix时间字段。因此“刚刚更新”仅表示刚收到坐标，不能单独证明坐标刚由卫星测得、用户仍在原地或后台采集持续正常。当前官方客户端会过滤部分过旧定位，但服务端并不验证设备传感器来源。展示accuracy和最后更新时间比只显示一个地图标记更有依据。

Android刷新当前范围时会保存请求前的scope与session，响应返回后再次核对，避免原聊天的结果覆盖用户新打开的聊天。iOS当前refreshLiveLocations方法没有同等响应后scope/token核对，需要把换账号、切换聊天时旧请求晚到加入验收，不能直接承诺两端异步隔离行为完全一致。依据：`android-app/app/src/main/java/cn/thestarline/workbench/AppState.kt:374-384`、`ios-app/Sources/Features/Chat/ChatStore.swift:577-586`。

## 停止失败、权限关闭与数据保留

停止本机传感器不会自动删除服务器最后坐标，服务器删除成功也不等于系统定位监听必定已释放。两端普通页面的结束动作若DELETE失败，会保留共享状态并展示或传播错误，不应显示“已经结束”。Android持续通知的“结束全部”是另一个分支：先停止本机采集，随后DELETE所有范围，网络失败约每10秒重试，通知显示正在重试。

系统权限关闭后，本机停止或无法继续采集，但远端记录不会因此自动消失；Android明确提示打开App结束原有共享。网络暂时不可用时保留最后成功位置和updatedAt，地图应标明最后更新时间，而不是宣称仍在实时移动。依据：`android-app/app/src/main/java/cn/thestarline/workbench/LiveLocationService.kt:54-65,91-130`、`ios-app/Sources/Features/Chat/ChatStore.swift:607-645`。

当前live_locations没有expiresAt或自动过期清理字段，也不保存一张连续轨迹历史表：同一范围用户更新覆盖该行的当前坐标。DELETE停止会删除该行；屏蔽或删除好友清理对应单聊双方的位置，退出/移除群成员清理该成员的位置，解散群清理该范围全部位置。备份、日志和系统缓存的生命周期不等同于该表行删除，不能据此承诺所有副本立即销毁。依据：`chat-service/src/database.mjs:693-725,928-934,990-1033,1063-1085`。

退出账号也需区分：Android先停止本机，再尽力清理服务器共享；iOS本机退出先停止manager，但其signOut路径没有同等逐范围DELETE共享流程。聊天session到期不会自动清掉live_locations，仍有权的其他成员可能读到旧位置，直至显式停止或关系清理。此为源码边界，未据此执行真实定位请求。详见[客户端生命周期](https://document.thestarline.cn/app/client-lifecycle.md)、[限制与错误](https://document.thestarline.cn/app/limits-and-errors.md)与[安全和隐私](https://document.thestarline.cn/app/security-and-privacy.md)。

源码核对范围：2026-10-23 的当前实现。本轮未执行生产业务请求或原生设备验收。
