欢迎光临
我们一直在努力

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

【腾讯位置服务开发者征文大赛】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

    前后端共享的核心类型如下:

    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[];
    };

    这里没有把结果直接做成字符串,而是返回结构化数据。这样前端可以很自然地渲染地图点位、列表卡片、筛选标签和推荐状态。

    四、第一步:定位之后先做逆地址解析

    移动端拿到的定位结果通常只是经纬度,例如:

    {
    "lat": 34.2467,
    "lng": 108.9138
    }

    但对用户来说,“108.9138, 34.2467”没有意义。用户真正想看到的是“你在西北工业大学友谊校区附近”或者“你在友谊西路附近”。

    所以定位成功后,我会先调用腾讯位置服务逆地址解析接口:

    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 可能叫:

    • 公共厕所
    • 公厕
    • 卫生间
    • 洗手间
    • 商场卫生间
    • 地铁站卫生间
    • 公共服务设施

    所以厕急达不会只查一个关键词,而是组合搜索。

    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);
    }

    底层地点搜索封装如下:

    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 分钟内优先
    可信得分 名称和分类里明确出现“厕所 / 卫生间 / 公厕”的优先

    简化后的评分代码如下:

    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 解析用户的口语化偏好

    基础模式下,用户打开应用就能看到附近厕所。但如果用户输入一句话,比如: 在这里插入图片描述

    我在西北工业大学附近,想找一个步行十分钟内、最好在商场或者地铁站里的卫生间。

    系统就需要把这句话拆成结构化偏好。

    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 和真实路线数据上。

    八、第五步:点击结果后规划步行路线

    用户在底部卡片里选中一个厕所后,应用会调用步行路线规划接口,计算从当前位置到目标点的路线。 在这里插入图片描述

    在这里插入图片描述

    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 米 / 去这里”。
  • 移动端页面里,我没有把路线步骤全部摊开。因为找厕所时,用户首先需要确认方向和距离,而不是阅读一长串文字。详细步骤可以折叠在“路线详情”里,需要时再展开。

    九、移动端地图交互:重点不是炫技,是别挡路

    移动端地图有一个常见问题:地图、列表、按钮很容易互相抢空间。

    厕急达的交互布局采用“地图在上,卡片在下”的结构:

    +————————–+
    | 顶部定位条 |
    +————————–+
    | |
    | 腾讯地图 |
    | 当前位置 / 厕所点位 |
    | |
    +————————–+
    | 推荐厕所卡片 |
    | 距离 / 类型 / 去这里 |
    +————————–+

    地图点位使用不同样式区分:

    点位样式
    当前位置 蓝色定位点
    推荐厕所 高亮标记
    其他候选 普通标记
    已选路线 蓝色粗线

    前端渲染路线的代码类似:

    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 字段:

    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 后,我更明显地感受到:好的位置服务应用不一定要很复杂。很多时候,一个足够具体的小场景,只要把用户当下的真实问题解决好,就能做出很强的实用感。

    在这里插入图片描述

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

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

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

    赞(0)
    未经允许不得转载:171主机测评 » 【腾讯位置服务开发者征文大赛】AI厕急达:我用腾讯位置服务做了一个移动端找厕所AI助手
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址