技术文档

城市提示与实时位置共享

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

三条位置路径

路径 数据与用途 是否自动开始实时共享
城市提示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,仍有权的其他成员可能读到旧位置,直至显式停止或关系清理。此为源码边界,未据此执行真实定位请求。详见客户端生命周期、限制与错误与安全和隐私。

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

Markdown 原文完整文档文本