【腾讯位置服务开发者征文大赛】AI厕急达:我用腾讯位置服务做了一个移动端找厕所AI助手
项目名称:厕急达 ToiletGo
应用形态:移动端 H5 / App WebView / 小程序均可迁移
技术方向:腾讯位置服务 + 移动端定位 + 附近 POI 检索 + 步行导航 + AI 偏好解析
项目地址:https://gitcode.com/mrdeam/ToiletGo.git
视频演示
AI厕急达:我用腾讯位置服务做了一个移动端找厕所AI助手
写在前面:找厕所这件小事,其实很适合做位置服务 Demo
很多地图类 Demo 喜欢做路线规划、附近美食、周边景点,这些当然都是很标准的场景。但我做第二个参赛作品时,反而想选一个更日常、更急迫、也更有移动端特点的问题:在外面突然想上厕所,怎么最快找到一个靠谱的卫生间?
这个需求看起来很小,但它非常真实。
你可能在景区、商圈、地铁口、大学校园、医院附近,也可能只是走在一条陌生街道上。这个时候用户通常不会有耐心慢慢筛选,也不想看一堆复杂信息。用户真正需要的是:
- 我现在在哪里?
- 附近最近的厕所在哪里?
- 步行过去要多久?
- 是公共厕所、商场卫生间,还是地铁站卫生间?
- 能不能一键导航?
- 如果我带小孩、老人,能不能优先找商场、医院、公共服务设施里的厕所?
所以我做了一个移动端应用构想:厕急达 ToiletGo。
它不是简单地搜索“厕所”两个字,而是围绕移动端的急用场景,把腾讯位置服务的定位、逆地址解析、地点搜索、步行路线规划和地图展示串成一个完整体验:用户打开页面,授权定位,系统自动搜索附近厕所,按“步行可达 + 类型可靠 + 距离合理”排序,并给出一键导航入口。
一、为什么这个场景适合移动端
找厕所这个需求和桌面端关系不大,它几乎天然发生在手机上。
用户不会在电脑前提前规划“十分钟后我要去哪里上厕所”。多数情况下,这个需求突然出现,而且伴随明显的时间压力。也就是说,应用设计不能像普通地图页面那样让用户慢慢输入、慢慢比较,而要尽量减少操作步骤。
我给厕急达定了三个移动端原则:
- 打开即定位:用户不需要手动输入起点,当前位置就是默认起点。
- 搜索即排序:不是把所有 POI 扔给用户,而是先给出最值得去的几个。
- 点击即出发:每个结果都要有明确的步行时间、距离、类型和导航按钮。
这个场景也很适合体现腾讯位置服务的价值。因为它不是只用一个接口,而是需要多个能力配合:
| 腾讯位置服务能力 | 在厕急达中的作用 |
|---|---|
| 定位能力 | 获取用户当前位置,作为搜索中心点 |
| 逆地址解析 | 把坐标转换成用户能看懂的位置描述 |
| 地点搜索 | 检索附近公厕、卫生间、商场、地铁站等 POI |
| 步行路线规划 | 计算从当前位置到目标厕所的步行路径和耗时 |
| 地图展示 | 在移动端地图上展示候选点位和推荐路线 |
这里最重要的不是“接口调通”,而是把接口组织成一个能解决具体问题的流程。
二、应用最终体验设计
厕急达的首页只有一个目标:让用户尽快找到可去的厕所。
我没有做复杂的首页宣传,也没有做大段功能介绍,而是把页面分成四块:
| 区域 | 内容 |
|---|---|
| 顶部定位条 | 展示当前位置、定位状态、刷新定位按钮 |
| 快捷筛选 | 最近优先、公共厕所优先、商场优先、地铁站优先、步行 10 分钟内 |
| 地图区域 | 展示当前位置、厕所点位、推荐路线 |
| 底部结果卡片 | 展示推荐厕所、距离、步行耗时、类型、导航按钮 |
用户打开应用后的典型流程是:
- 授权定位;
- 应用显示“你在西安市碑林区友谊西路附近”;
- 自动搜索 1.5 公里范围内的公厕和卫生间;
- 系统推荐 3 到 5 个结果;
- 用户点击其中一个,地图绘制步行路线;
- 用户点击“去这里”,跳转腾讯地图导航或使用内置路线展示。
为了让移动端体验更像真实工具,我把按钮文案设计得很直接:
- “最近的”
- “商场里”
- “地铁站”
- “步行 10 分钟”
- “重新定位”
- “去这里”
在急用场景里,文案越短越好。用户不是来研究系统的,而是要快速做决定。
三、整体技术架构
项目采用前后端分离结构。前端负责移动端交互和地图展示,后端负责封装腾讯位置服务 WebService API,避免在浏览器中暴露服务端 Key。
后端接口主要设计为三个:
| 接口 | 作用 |
|---|---|
POST /api/toilet/nearby | 根据当前位置搜索附近厕所 |
POST /api/toilet/route | 计算当前位置到某个厕所的步行路线 |
GET /api/config | 返回前端地图需要的客户端 Key |
前后端共享的核心类型如下:
exporttypeCoordinate={lat:number;lng:number;};exporttypeToiletPoi={id:string;title:string;address:string;category:string;distance:number;location:Coordinate;sourceKeyword:string;score:number;tags:string[];};exporttypeToiletSearchResult={type:"toilet_nearby";mode:"tencent"|"mock";center:Coordinate;currentAddress:string;radius:number;recommendedId:string;pois:ToiletPoi[];notices:string[];};这里没有把结果直接做成字符串,而是返回结构化数据。这样前端可以很自然地渲染地图点位、列表卡片、筛选标签和推荐状态。
四、第一步:定位之后先做逆地址解析
移动端拿到的定位结果通常只是经纬度,例如:
{"lat":34.2467,"lng":108.9138}但对用户来说,“108.9138, 34.2467”没有意义。用户真正想看到的是“你在西北工业大学友谊校区附近”或者“你在友谊西路附近”。
所以定位成功后,我会先调用腾讯位置服务逆地址解析接口:
constTENCENT_API="";asyncfunctionrequestTencent<T>(path:string,params:Record<string,string|number>):Promise<T>{constkey=process.env.TENCENT_MAP_KEY;if(!key){thrownewError("TENCENT_MAP_KEY is required");}consturl=newURL(`${TENCENT_API}${path}`);Object.entries({...params,key}).forEach(([name,value])=>{url.searchParams.set(name,String(value));});constresponse=awaitfetch(url);constdata=awaitresponse.json();if(!response.ok||data.status!==0){thrownewError(data.message||"Tencent location service request failed");}returndataasT;}exportasyncfunctionreverseGeocoder(location:Coordinate){constdata=awaitrequestTencent<any>("/ws/geocoder/v1/",{location:`${location.lat},${location.lng}`,get_poi:1});return{address:data.result.address,formattedAddresses:data.result.formatted_addresses,nearbyPois:data.result.pois||[]};}这一层主要解决两个问题:
- 给用户一个确定感:应用知道我现在大概在哪;
- 给搜索一个上下文:后续可以根据当前位置周边环境做更合理的推荐。
五、第二步:不是只搜“厕所”,而是组合关键词搜索
真实开发时我发现,如果只搜索一个关键词“厕所”,结果并不总是稳定。不同城市、不同商圈、不同数据来源里,相关 POI 可能叫:
- 公共厕所
- 公厕
- 卫生间
- 洗手间
- 商场卫生间
- 地铁站卫生间
- 公共服务设施
所以厕急达不会只查一个关键词,而是组合搜索。
consttoiletKeywords=["公共厕所","公厕","卫生间","洗手间","商场","地铁站"];exportasyncfunctionsearchNearbyToilets(center:Coordinate,radius=1500):Promise<ToiletPoi[]>{consttasks=toiletKeywords.map((keyword)=>searchNearbyPois(center,keyword,radius).then((items)=>items.map((item)=>({...item,sourceKeyword:keyword}))));constsettled=awaitPromise.allSettled(tasks);constpois=settled.flatMap((item)=>item.status==="fulfilled"?item.value:[]);returndedupePois(pois).map(enhanceToiletPoi);}底层地点搜索封装如下:
asyncfunctionsearchNearbyPois(center:Coordinate,keyword:string,radius:number){constboundary=`nearby(${center.lat},${center.lng},${radius})`;constdata=awaitrequestTencent<any>("/ws/place/v1/search",{keyword,boundary,page_size:20});return(data.data||[]).map((item:any)=>({id:item.id||`${item.title}-${item.address}`,title:item.title,address:item.address||"",category:item.category||"",distance:Number(item._distance||0),location:{lat:item.location.lat,lng:item.location.lng}}));}这里我使用了Promise.allSettled,原因很简单:找厕所是一个救急场景,某个关键词失败不应该导致整个页面失败。只要有一部分结果可用,就应该先展示出来。
六、第三步:给厕所排序,而不是让用户自己猜
搜索出来的 POI 不能直接全部展示给用户。因为用户不是来做数据筛选的,用户要的是“哪个最值得去”。
我给排序模型设计了四个维度:
| 指标 | 说明 |
|---|---|
| 距离得分 | 越近越好,但不是唯一标准 |
| 类型得分 | 公共厕所、商场、地铁站、医院等更容易作为目标 |
| 可达得分 | 步行 10 分钟内优先 |
| 可信得分 | 名称和分类里明确出现“厕所 / 卫生间 / 公厕”的优先 |
简化后的评分代码如下:
functionscoreToiletPoi(poi:ToiletPoi){constdistanceScore=calcDistanceScore(poi.distance);consttypeScore=calcTypeScore(poi);constreachableScore=poi.distance<=800?100:poi.distance<=1200?75:45;constconfidenceScore=calcConfidenceScore(poi);returnMath.round(distanceScore*0.36+typeScore*0.28+reachableScore*0.2+confidenceScore*0.16);}functioncalcDistanceScore(distance:number){if(distance<=200)return100;if(distance<=500)return88;if(distance<=800)return72;if(distance<=1200)return55;return35;}functioncalcTypeScore(poi:ToiletPoi){consttext=`${poi.title}${poi.category}${poi.sourceKeyword}`;if(/公共厕所|公厕|卫生间|洗手间/.test(text))return100;if(/商场|购物中心|医院|地铁站/.test(text))return78;if(/公园|景区|广场|车站/.test(text))return68;return45;}functioncalcConfidenceScore(poi:ToiletPoi){consttext=`${poi.title}${poi.address}${poi.category}`;return/厕所|公厕|卫生间|洗手间/.test(text)?100:60;}这里有一个产品取舍:最近的不一定永远排第一。
比如 180 米外有一个名称模糊的小 POI,600 米外有一个明确标注为“公共厕所”的点位,那么后者可能更值得推荐。移动端找厕所不仅要近,还要减少用户走到附近却找不到入口的概率。
七、第四步:用 AI 解析用户的口语化偏好
基础模式下,用户打开应用就能看到附近厕所。但如果用户输入一句话,比如:
我在西北工业大学附近,想找一个步行十分钟内、最好在商场或者地铁站里的卫生间。系统就需要把这句话拆成结构化偏好。
exporttypeToiletIntent={originText?:string;radius:number;maxWalkMinutes?:number;preferIndoor:boolean;preferTransit:boolean;preferPublicToilet:boolean;needAccessible:boolean;rawText:string;};exportfunctionparseToiletIntent(text:string):ToiletIntent{return{originText:extractOrigin(text),radius:/附近|周边/.test(text)?1500:1000,maxWalkMinutes:/十分钟|10分钟/.test(text)?10:undefined,preferIndoor:/商场|购物中心|室内|干净/.test(text),preferTransit:/地铁|车站|公交/.test(text),preferPublicToilet:/公厕|公共厕所/.test(text),needAccessible:/无障碍|老人|轮椅/.test(text),rawText:text};}这一版 Demo 可以先用规则解析保证稳定,后续再替换成大模型 Tool Calling。真正重要的是工程分层:AI 只负责理解用户偏好,腾讯位置服务负责提供真实位置数据,排序模块负责做可复现的推荐。
我不希望模型直接编造“某某地方有卫生间”。在位置类应用里,所有推荐都应该落在真实 POI 和真实路线数据上。
八、第五步:点击结果后规划步行路线
用户在底部卡片里选中一个厕所后,应用会调用步行路线规划接口,计算从当前位置到目标点的路线。
exportasyncfunctionplanWalkingRoute(from:Coordinate,to:Coordinate){constdata=awaitrequestTencent<any>("/ws/direction/v1/walking/",{from:`${from.lat},${from.lng}`,to:`${to.lat},${to.lng}`});constroute=data.result.routes[0];return{distance:Number(route.distance||0),duration:Number(route.duration||0)*60,polyline:decodeTencentPolyline(route.polyline),steps:Array.isArray(route.steps)?route.steps.map((step:any)=>step.instruction||"步行"):[]};}前端拿到路线后做两件事:
- 地图上高亮从当前位置到目标厕所的步行路线;
- 底部卡片显示“约 6 分钟 / 420 米 / 去这里”。
移动端页面里,我没有把路线步骤全部摊开。因为找厕所时,用户首先需要确认方向和距离,而不是阅读一长串文字。详细步骤可以折叠在“路线详情”里,需要时再展开。
九、移动端地图交互:重点不是炫技,是别挡路
移动端地图有一个常见问题:地图、列表、按钮很容易互相抢空间。
厕急达的交互布局采用“地图在上,卡片在下”的结构:
+--------------------------+ | 顶部定位条 | +--------------------------+ | | | 腾讯地图 | | 当前位置 / 厕所点位 | | | +--------------------------+ | 推荐厕所卡片 | | 距离 / 类型 / 去这里 | +--------------------------+地图点位使用不同样式区分:
| 点位 | 样式 |
|---|---|
| 当前位置 | 蓝色定位点 |
| 推荐厕所 | 高亮标记 |
| 其他候选 | 普通标记 |
| 已选路线 | 蓝色粗线 |
前端渲染路线的代码类似:
functionrenderWalkingRoute(map:TMap.Map,route:WalkingRoute){returnnewTMap.MultiPolyline({map,styles:{walking:newTMap.PolylineStyle({color:"#1769e0",width:8,borderWidth:2,borderColor:"#ffffff"})},geometries:[{id:"selected-walking-route",styleId:"walking",paths:route.polyline.map((point)=>newTMap.LatLng(point.lat,point.lng))}]});}移动端还有一个细节:底部卡片不能太高。
如果卡片占据半屏,用户看不到路线;如果信息太少,用户又不敢点。所以我只放四类关键信息:
- 名称:比如“西安大悦城卫生间”
- 距离:比如“420 米”
- 预计步行:比如“约 6 分钟”
- 推荐原因:比如“商场内,名称匹配卫生间,步行距离较近”
十、接口回退与异常处理
找厕所这种应用,异常处理比普通 Demo 更重要。因为用户打开应用时可能真的很急,如果页面只是显示一个技术错误,会非常糟糕。
我处理了几类常见异常:
| 异常 | 处理方式 |
|---|---|
| 用户拒绝定位 | 提供手动输入当前位置入口 |
| 定位失败 | 使用上一次定位或提示重新定位 |
| 腾讯接口限流 | 展示缓存结果或模拟数据,并提示稍后重试 |
| 附近无明确厕所 | 扩大搜索半径,补充商场、地铁站、医院等公共设施 |
| 步行路线失败 | 仍展示点位和直线距离,允许跳转外部地图 |
后端返回结构里保留notices字段:
return{type:"toilet_nearby",mode:"tencent",center,currentAddress,radius,recommendedId,pois,notices:["结果基于腾讯位置服务地点搜索返回。","部分商场、地铁站内卫生间可能需要进入建筑后按现场指引查找。"]};这个提示很有必要。因为 POI 能告诉我们“附近有相关设施”,但建筑内部具体入口、开放状态、维护状态仍然可能变化。位置服务应用不能把数据能力说成现实保证。
十一、一次真实测试流程
我用下面这个场景做测试:
当前位置:西安市西北工业大学友谊校区附近 需求:找一个步行十分钟内、最好在商场或者地铁站附近的卫生间系统执行流程如下:
- 获取当前位置坐标;
- 调用逆地址解析,得到当前位置描述;
- 以当前位置为中心搜索“公共厕所 / 公厕 / 卫生间 / 洗手间 / 商场 / 地铁站”;
- 对结果去重;
- 根据距离、类型、关键词匹配和可达性打分;
- 推荐排名最高的 3 个结果;
- 用户选择后,调用步行路线规划并在地图上绘制路线。
结果卡片示例:
| 推荐 | 类型 | 距离 | 步行时间 | 推荐理由 |
|---|---|---|---|---|
| 附近公共厕所 | 公共厕所 | 约 350 米 | 约 5 分钟 | 名称明确,距离较近 |
| 商场卫生间 | 商场设施 | 约 620 米 | 约 9 分钟 | 室内场所,更容易按指引寻找 |
| 地铁站卫生间 | 交通设施 | 约 760 米 | 约 11 分钟 | 适合继续换乘或出行 |
这个结果比“附近厕所列表”更进一步:它不仅告诉用户哪里有厕所,还告诉用户为什么推荐这个点,以及从当前位置走过去大概要多久。
十二、开发过程中踩过的坑
12.1 “厕所”关键词太窄
一开始我只搜索“厕所”,结果发现有些地方的数据名称是“公共厕所”,有些是“卫生间”,还有些商场不会直接把卫生间作为独立 POI 返回。
后来我改成组合关键词,并把商场、地铁站、医院、公园这类公共设施作为补充候选。这样做之后,结果覆盖明显更稳定。
12.2 最近不一定最好
找厕所很容易直觉上选择最近点,但真实使用不一定如此。一个名称模糊、入口不清晰的小点位,可能不如稍远一点但更明确的公共厕所或商场卫生间。
所以排序时我没有只按距离,而是加入类型和可信度权重。
12.3 移动端底部卡片不能堆信息
PC 页面可以放很多评分、表格和解释,但手机页面不行。尤其找厕所这种急用场景,用户不需要读长文。
最后我把底部卡片压缩成“名称、距离、时间、推荐理由、按钮”五个元素,其他信息都折叠起来。
12.4 POI 数据不等于实时开放状态
厕所是否开放、是否维修、是否在商场内部、是否需要进入闸机,这些信息不是每一次地点搜索都能完全覆盖。
所以页面文案必须克制,只说“附近检索到相关设施”,不承诺“一定可用”。这和做路线安全类应用一样,真实世界的复杂性不能被一个分数掩盖。
十三、项目创新点
13.1 把高频小需求做成完整位置服务闭环
找厕所不是宏大的功能,但它非常高频,也非常依赖位置服务。这个项目把定位、逆地址解析、地点搜索、路线规划和地图展示串成完整闭环,比单独展示某个 API 更能体现腾讯位置服务的实际应用价值。
13.2 从“附近搜索”升级为“可行动推荐”
普通附近搜索只是给用户一个列表。厕急达进一步做了排序、标签、推荐理由和步行路线,让结果从“你自己看”变成“现在可以去这里”。
13.3 移动端优先,而不是桌面页面缩小
应用从一开始就按手机场景设计:打开即定位、卡片少信息、一键导航、地图和列表协同。它不是把 PC 页面响应式压缩,而是围绕用户当下动作重新组织界面。
13.4 AI 只做偏好解析,不编造位置事实
用户可以用自然语言说“想找干净点、商场里的、十分钟内的卫生间”。AI 负责把这句话变成筛选条件,真实地点仍然由腾讯位置服务返回。这样既有自然语言交互的便利,也避免模型凭空生成不存在的地点。
十四、后续优化方向
这个 Demo 还可以继续做很多增强:
- 增加无障碍厕所、母婴室、第三卫生间等标签识别;
- 接入用户反馈,让用户标记“已找到 / 未找到 / 正在维修”;
- 支持离线缓存常用商圈、公园、景区的公共厕所点位;
- 增加“带老人 / 带小孩 / 景区模式”等场景偏好;
- 支持跳转腾讯地图 App 继续导航;
- 接入大模型 Tool Calling,让口语化需求解析更自然;
- 在小程序端增加“附近 500 米一键找厕所”快捷入口。
结语
做完厕急达这个 Demo 后,我更明显地感受到:好的位置服务应用不一定要很复杂。很多时候,一个足够具体的小场景,只要把用户当下的真实问题解决好,就能做出很强的实用感。
腾讯位置服务在这个项目里承担了非常核心的底座作用:定位让应用知道用户在哪里,逆地址解析让坐标变得可读,地点搜索找到附近可用设施,步行路线规划把目标变成可到达路径,地图展示则让用户在手机上快速确认方向。
从“附近有什么”到“现在应该去哪里”,这是厕急达想完成的一步。
如果你也在做移动端位置服务应用,不妨从身边这些看似很小、但真实存在的需求开始。它们往往比一个大而全的功能更容易做出用户价值。