【腾讯位置服务开发者征文大赛】AI厕急达:我用腾讯位置服务做了一个移动端找厕所AI助手
项目名称:厕急达 ToiletGo
应用形态:移动端 H5 / App WebView / 小程序均可迁移
技术方向:腾讯位置服务 + 移动端定位 + 附近 POI 检索 + 步行导航 + 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 |
前后端共享的核心类型如下:
ts
export type Coordinate = {
lat: number;
lng: number;
};
export type ToiletPoi = {
id: string;
title: string;
address: string;
category: string;
distance: number;
location: Coordinate;
sourceKeyword: string;
score: number;
tags: string[];
};
export type ToiletSearchResult = {
type: "toilet_nearby";
mode: "tencent" | "mock";
center: Coordinate;
currentAddress: string;
radius: number;
recommendedId: string;
pois: ToiletPoi[];
notices: string[];
};
这里没有把结果直接做成字符串,而是返回结构化数据。这样前端可以很自然地渲染地图点位、列表卡片、筛选标签和推荐状态。
四、第一步:定位之后先做逆地址解析
移动端拿到的定位结果通常只是经纬度,例如:
json
{
"lat": 34.2467,
"lng": 108.9138
}
但对用户来说,"108.9138, 34.2467"没有意义。用户真正想看到的是"你在西北工业大学友谊校区附近"或者"你在友谊西路附近"。
所以定位成功后,我会先调用腾讯位置服务逆地址解析接口:
ts
const TENCENT_API = "";
async function requestTencent<T>(
path: string,
params: Record<string, string | number>
): Promise<T> {
const key = process.env.TENCENT_MAP_KEY;
if (!key) {
throw new Error("TENCENT_MAP_KEY is required");
}
const url = new URL(`${TENCENT_API}${path}`);
Object.entries({ ...params, key }).forEach(([name, value]) => {
url.searchParams.set(name, String(value));
});
const response = await fetch(url);
const data = await response.json();
if (!response.ok || data.status !== 0) {
throw new Error(data.message || "Tencent location service request failed");
}
return data as T;
}
export async function reverseGeocoder(location: Coordinate) {
const data = await requestTencent<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 可能叫:
- 公共厕所
- 公厕
- 卫生间
- 洗手间
- 商场卫生间
- 地铁站卫生间
- 公共服务设施
所以厕急达不会只查一个关键词,而是组合搜索。
ts
const toiletKeywords = [
"公共厕所",
"公厕",
"卫生间",
"洗手间",
"商场",
"地铁站"
];
export async function searchNearbyToilets(
center: Coordinate,
radius = 1500
): Promise<ToiletPoi[]> {
const tasks = toiletKeywords.map((keyword) =>
searchNearbyPois(center, keyword, radius)
.then((items) =>
items.map((item) => ({
...item,
sourceKeyword: keyword
}))
)
);
const settled = await Promise.allSettled(tasks);
const pois = settled.flatMap((item) =>
item.status === "fulfilled" ? item.value : []
);
return dedupePois(pois).map(enhanceToiletPoi);
}
底层地点搜索封装如下:
ts
async function searchNearbyPois(
center: Coordinate,
keyword: string,
radius: number
) {
const boundary = `nearby(${center.lat},${center.lng},${radius})`;
const data = await requestTencent<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 分钟内优先 |
| 可信得分 | 名称和分类里明确出现"厕所 / 卫生间 / 公厕"的优先 |
简化后的评分代码如下:
ts
function scoreToiletPoi(poi: ToiletPoi) {
const distanceScore = calcDistanceScore(poi.distance);
const typeScore = calcTypeScore(poi);
const reachableScore = poi.distance <= 800 ? 100 : poi.distance <= 1200 ? 75 : 45;
const confidenceScore = calcConfidenceScore(poi);
return Math.round(
distanceScore * 0.36 +
typeScore * 0.28 +
reachableScore * 0.2 +
confidenceScore * 0.16
);
}
function calcDistanceScore(distance: number) {
if (distance <= 200) return 100;
if (distance <= 500) return 88;
if (distance <= 800) return 72;
if (distance <= 1200) return 55;
return 35;
}
function calcTypeScore(poi: ToiletPoi) {
const text = `${poi.title} ${poi.category} ${poi.sourceKeyword}`;
if (/公共厕所|公厕|卫生间|洗手间/.test(text)) return 100;
if (/商场|购物中心|医院|地铁站/.test(text)) return 78;
if (/公园|景区|广场|车站/.test(text)) return 68;
return 45;
}
function calcConfidenceScore(poi: ToiletPoi) {
const text = `${poi.title} ${poi.address} ${poi.category}`;
return /厕所|公厕|卫生间|洗手间/.test(text) ? 100 : 60;
}
这里有一个产品取舍:最近的不一定永远排第一。

比如 180 米外有一个名称模糊的小 POI,600 米外有一个明确标注为"公共厕所"的点位,那么后者可能更值得推荐。移动端找厕所不仅要近,还要减少用户走到附近却找不到入口的概率。
七、第四步:用 AI 解析用户的口语化偏好
基础模式下,用户打开应用就能看到附近厕所。但如果用户输入一句话,比如: 
text
我在西北工业大学附近,想找一个步行十分钟内、最好在商场或者地铁站里的卫生间。
系统就需要把这句话拆成结构化偏好。
ts
export type ToiletIntent = {
originText?: string;
radius: number;
maxWalkMinutes?: number;
preferIndoor: boolean;
preferTransit: boolean;
preferPublicToilet: boolean;
needAccessible: boolean;
rawText: string;
};
export function parseToiletIntent(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 和真实路线数据上。
八、第五步:点击结果后规划步行路线
用户在底部卡片里选中一个厕所后,应用会调用步行路线规划接口,计算从当前位置到目标点的路线。 

ts
export async function planWalkingRoute(from: Coordinate, to: Coordinate) {
const data = await requestTencent<any>("/ws/direction/v1/walking/", {
from: `${from.lat},${from.lng}`,
to: `${to.lat},${to.lng}`
});
const route = 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 米 / 去这里"。
移动端页面里,我没有把路线步骤全部摊开。因为找厕所时,用户首先需要确认方向和距离,而不是阅读一长串文字。详细步骤可以折叠在"路线详情"里,需要时再展开。
九、移动端地图交互:重点不是炫技,是别挡路
移动端地图有一个常见问题:地图、列表、按钮很容易互相抢空间。
厕急达的交互布局采用"地图在上,卡片在下"的结构:
text
+--------------------------+
| 顶部定位条 |
+--------------------------+
| |
| 腾讯地图 |
| 当前位置 / 厕所点位 |
| |
+--------------------------+
| 推荐厕所卡片 |
| 距离 / 类型 / 去这里 |
+--------------------------+
地图点位使用不同样式区分:
| 点位 | 样式 |
|---|---|
| 当前位置 | 蓝色定位点 |
| 推荐厕所 | 高亮标记 |
| 其他候选 | 普通标记 |
| 已选路线 | 蓝色粗线 |
前端渲染路线的代码类似:
ts
function renderWalkingRoute(map: TMap.Map, route: WalkingRoute) {
return new TMap.MultiPolyline({
map,
styles: {
walking: new TMap.PolylineStyle({
color: "#1769e0",
width: 8,
borderWidth: 2,
borderColor: "#ffffff"
})
},
geometries: [
{
id: "selected-walking-route",
styleId: "walking",
paths: route.polyline.map(
(point) => new TMap.LatLng(point.lat, point.lng)
)
}
]
});
}
移动端还有一个细节:底部卡片不能太高。
如果卡片占据半屏,用户看不到路线;如果信息太少,用户又不敢点。所以我只放四类关键信息:
- 名称:比如"西安大悦城卫生间"
- 距离:比如"420 米"
- 预计步行:比如"约 6 分钟"
- 推荐原因:比如"商场内,名称匹配卫生间,步行距离较近"
十、接口回退与异常处理
找厕所这种应用,异常处理比普通 Demo 更重要。因为用户打开应用时可能真的很急,如果页面只是显示一个技术错误,会非常糟糕。
我处理了几类常见异常:
| 异常 | 处理方式 |
|---|---|
| 用户拒绝定位 | 提供手动输入当前位置入口 |
| 定位失败 | 使用上一次定位或提示重新定位 |
| 腾讯接口限流 | 展示缓存结果或模拟数据,并提示稍后重试 |
| 附近无明确厕所 | 扩大搜索半径,补充商场、地铁站、医院等公共设施 |
| 步行路线失败 | 仍展示点位和直线距离,允许跳转外部地图 |
后端返回结构里保留 notices 字段:
ts
return {
type: "toilet_nearby",
mode: "tencent",
center,
currentAddress,
radius,
recommendedId,
pois,
notices: [
"结果基于腾讯位置服务地点搜索返回。",
"部分商场、地铁站内卫生间可能需要进入建筑后按现场指引查找。"
]
};
这个提示很有必要。因为 POI 能告诉我们"附近有相关设施",但建筑内部具体入口、开放状态、维护状态仍然可能变化。位置服务应用不能把数据能力说成现实保证。
十一、一次真实测试流程
我用下面这个场景做测试:
text
当前位置:西安市西北工业大学友谊校区附近
需求:找一个步行十分钟内、最好在商场或者地铁站附近的卫生间
系统执行流程如下:
- 获取当前位置坐标;
- 调用逆地址解析,得到当前位置描述;
- 以当前位置为中心搜索"公共厕所 / 公厕 / 卫生间 / 洗手间 / 商场 / 地铁站";
- 对结果去重;
- 根据距离、类型、关键词匹配和可达性打分;
- 推荐排名最高的 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 后,我更明显地感受到:好的位置服务应用不一定要很复杂。很多时候,一个足够具体的小场景,只要把用户当下的真实问题解决好,就能做出很强的实用感。

腾讯位置服务在这个项目里承担了非常核心的底座作用:定位让应用知道用户在哪里,逆地址解析让坐标变得可读,地点搜索找到附近可用设施,步行路线规划把目标变成可到达路径,地图展示则让用户在手机上快速确认方向。
从"附近有什么"到"现在应该去哪里",这是厕急达想完成的一步。
如果你也在做移动端位置服务应用,不妨从身边这些看似很小、但真实存在的需求开始。它们往往比一个大而全的功能更容易做出用户价值。
