【腾讯位置服务开发者征文大赛】AI厕急达:我用腾讯位置服务做了一个移动端找厕所AI助手

【腾讯位置服务开发者征文大赛】AI厕急达:我用腾讯位置服务做了一个移动端找厕所AI助手

项目名称:厕急达 ToiletGo

应用形态:移动端 H5 / App WebView / 小程序均可迁移

技术方向:腾讯位置服务 + 移动端定位 + 附近 POI 检索 + 步行导航 + AI 偏好解析

项目地址:gitcode.com/mrdeam/Toil...

写在前面:找厕所这件小事,其实很适合做位置服务 Demo

很多地图类 Demo 喜欢做路线规划、附近美食、周边景点,这些当然都是很标准的场景。但我做第二个参赛作品时,反而想选一个更日常、更急迫、也更有移动端特点的问题:在外面突然想上厕所,怎么最快找到一个靠谱的卫生间?

这个需求看起来很小,但它非常真实。

你可能在景区、商圈、地铁口、大学校园、医院附近,也可能只是走在一条陌生街道上。这个时候用户通常不会有耐心慢慢筛选,也不想看一堆复杂信息。用户真正需要的是:

  • 我现在在哪里?
  • 附近最近的厕所在哪里?
  • 步行过去要多久?
  • 是公共厕所、商场卫生间,还是地铁站卫生间?
  • 能不能一键导航?
  • 如果我带小孩、老人,能不能优先找商场、医院、公共服务设施里的厕所?

所以我做了一个移动端应用构想:厕急达 ToiletGo

它不是简单地搜索"厕所"两个字,而是围绕移动端的急用场景,把腾讯位置服务的定位、逆地址解析、地点搜索、步行路线规划和地图展示串成一个完整体验:用户打开页面,授权定位,系统自动搜索附近厕所,按"步行可达 + 类型可靠 + 距离合理"排序,并给出一键导航入口。

一、为什么这个场景适合移动端

找厕所这个需求和桌面端关系不大,它几乎天然发生在手机上。

用户不会在电脑前提前规划"十分钟后我要去哪里上厕所"。多数情况下,这个需求突然出现,而且伴随明显的时间压力。也就是说,应用设计不能像普通地图页面那样让用户慢慢输入、慢慢比较,而要尽量减少操作步骤。

我给厕急达定了三个移动端原则:

  1. 打开即定位:用户不需要手动输入起点,当前位置就是默认起点。
  2. 搜索即排序:不是把所有 POI 扔给用户,而是先给出最值得去的几个。
  3. 点击即出发:每个结果都要有明确的步行时间、距离、类型和导航按钮。

这个场景也很适合体现腾讯位置服务的价值。因为它不是只用一个接口,而是需要多个能力配合:

腾讯位置服务能力 在厕急达中的作用
定位能力 获取用户当前位置,作为搜索中心点
逆地址解析 把坐标转换成用户能看懂的位置描述
地点搜索 检索附近公厕、卫生间、商场、地铁站等 POI
步行路线规划 计算从当前位置到目标厕所的步行路径和耗时
地图展示 在移动端地图上展示候选点位和推荐路线

这里最重要的不是"接口调通",而是把接口组织成一个能解决具体问题的流程。

二、应用最终体验设计

厕急达的首页只有一个目标:让用户尽快找到可去的厕所。

我没有做复杂的首页宣传,也没有做大段功能介绍,而是把页面分成四块:

区域 内容
顶部定位条 展示当前位置、定位状态、刷新定位按钮
快捷筛选 最近优先、公共厕所优先、商场优先、地铁站优先、步行 10 分钟内
地图区域 展示当前位置、厕所点位、推荐路线
底部结果卡片 展示推荐厕所、距离、步行耗时、类型、导航按钮

用户打开应用后的典型流程是:

  1. 授权定位;
  2. 应用显示"你在西安市碑林区友谊西路附近";
  3. 自动搜索 1.5 公里范围内的公厕和卫生间;
  4. 系统推荐 3 到 5 个结果;
  5. 用户点击其中一个,地图绘制步行路线;
  6. 用户点击"去这里",跳转腾讯地图导航或使用内置路线展示。

为了让移动端体验更像真实工具,我把按钮文案设计得很直接:

  • "最近的"
  • "商场里"
  • "地铁站"
  • "步行 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 || []
  };
}

这一层主要解决两个问题:

  1. 给用户一个确定感:应用知道我现在大概在哪;
  2. 给搜索一个上下文:后续可以根据当前位置周边环境做更合理的推荐。

五、第二步:不是只搜"厕所",而是组合关键词搜索

真实开发时我发现,如果只搜索一个关键词"厕所",结果并不总是稳定。不同城市、不同商圈、不同数据来源里,相关 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 || "步行")
      : []
  };
}

前端拿到路线后做两件事:

  1. 地图上高亮从当前位置到目标厕所的步行路线;
  2. 底部卡片显示"约 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 复制代码
当前位置:西安市西北工业大学友谊校区附近
需求:找一个步行十分钟内、最好在商场或者地铁站附近的卫生间

系统执行流程如下:

  1. 获取当前位置坐标;
  2. 调用逆地址解析,得到当前位置描述;
  3. 以当前位置为中心搜索"公共厕所 / 公厕 / 卫生间 / 洗手间 / 商场 / 地铁站";
  4. 对结果去重;
  5. 根据距离、类型、关键词匹配和可达性打分;
  6. 推荐排名最高的 3 个结果;
  7. 用户选择后,调用步行路线规划并在地图上绘制路线。

结果卡片示例:

推荐 类型 距离 步行时间 推荐理由
附近公共厕所 公共厕所 约 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 还可以继续做很多增强:

  1. 增加无障碍厕所、母婴室、第三卫生间等标签识别;
  2. 接入用户反馈,让用户标记"已找到 / 未找到 / 正在维修";
  3. 支持离线缓存常用商圈、公园、景区的公共厕所点位;
  4. 增加"带老人 / 带小孩 / 景区模式"等场景偏好;
  5. 支持跳转腾讯地图 App 继续导航;
  6. 接入大模型 Tool Calling,让口语化需求解析更自然;
  7. 在小程序端增加"附近 500 米一键找厕所"快捷入口。

结语

做完厕急达这个 Demo 后,我更明显地感受到:好的位置服务应用不一定要很复杂。很多时候,一个足够具体的小场景,只要把用户当下的真实问题解决好,就能做出很强的实用感。

腾讯位置服务在这个项目里承担了非常核心的底座作用:定位让应用知道用户在哪里,逆地址解析让坐标变得可读,地点搜索找到附近可用设施,步行路线规划把目标变成可到达路径,地图展示则让用户在手机上快速确认方向。

从"附近有什么"到"现在应该去哪里",这是厕急达想完成的一步。

如果你也在做移动端位置服务应用,不妨从身边这些看似很小、但真实存在的需求开始。它们往往比一个大而全的功能更容易做出用户价值。

相关推荐
欧雷殿2 小时前
适配一人公司!家庭局域网 AI 工作台来了
后端·agent·aiops
ltl2 小时前
梯度下降与反向传播
后端
老马95272 小时前
opencode6-桌面应用实战1
人工智能·后端
掘金者阿豪2 小时前
NAS搭好了但找不到资源?用Pansou同时搜几十个网盘,帮我省了不少会员钱
后端
第五页的你2 小时前
Go语言--一篇通
后端
数据仓库搬砖人2 小时前
DWS 列存表分区创建原理详解
后端
渐儿2 小时前
上下文工程 · 02 · 工具结果的反注入与信任边界
后端
得物技术2 小时前
基于 Harness + SDD + 多仓管理模式的 AI 全栈开发实践|得物技术
前端·人工智能·后端
掘金者阿豪3 小时前
服务器突然卡了却找不到原因?cAdvisor让每个容器都透明可见
后端