
简介这份代码包是一套基于Xposed框架的微信附近人采集工具源代码面向安卓逆向、Xposed插件开发及网络协议分析方向的中高级学习者。项目清晰拆分为Xposed钩子拦截、HTTP请求模拟、多线程数据处理与导出三个核心模块源码展示了如何Hook微信附近人界面获取UI上下文如何构造并模拟微信客户端HTTP协议头请求原始数据以及如何利用多线程队列提升采集效率并通过POI将结果实时导出到Excel。资源共15个文件以6个Java源码文件为主线辅以5个Excel示例数据、XML配置、Markdown说明等压缩包仅38KB轻量易读适合对照源码拆解整体实现思路。目前已有78人学习下载对想要实际动手体验Xposed Hook、掌握安卓端数据采集与导出技巧的开发者颇具参考价值。需要提醒的是该工具涉及用户隐私数据仅供学习研究使用时应严格遵守相关法律法规与平台规则。 做LBS开发这些年见过不少人在微信生态里折腾“附近的人”“附近门店”“周边服务”这类功能。坦白讲凡是打着“采集”旗号、把用户头像昵称位置一键抓下来的工具不管源码写得再花哨本质上都在踩法律红线——微信用户的个人信息受法律保护未经授权批量抓取平台风控和法规两条线都会找上门。这篇博文不碰任何破解、逆向、绕过风控的灰产路子我把这个项目重新拆解成一个合规的LBS周边服务开发课题从微信生态能合法拿到的位置信息出发讲清楚坐标处理、距离计算、附近检索、可视化展示的完整技术链路并提供可直接运行的源码方案。适合正在做小程序LBS功能、门店推荐、社区服务的开发者参考尤其是那些被“附近的人”实现方案困扰过的朋友。1. 项目定位与合规边界1.1 真实需求与技术本质“附近人”这个功能技术上的本质就是“基于地理位置的近邻检索”。无论你打开微信的附近的人、点评的附近门店还是外卖的附近商家背后都遵循同一条逻辑链获取当前坐标、清洗坐标数据、计算与目标点的距离、按距离排序并渲染结果。很多人一上来就想做“采集工具”其实是把需求搞拧了。大多数业务场景要的不是偷偷抓一批陌生人数据而是“允许用户授权位置后查看周边有哪些人/店/服务”。这两者的技术难点看似相同——都要定位、都要算距离、都要做列表——但数据来源和合规路径完全不同。前者从微信私域接口硬抠属于对抗平台风控的黑盒操作后者走小程序官方位置API和自建服务端数据是白盒的、可审计的、能上线的方案。如果你要把这个项目做成能交付、能商用、敢放进简历的作品只有第二条路能走。1.2 合规红线与可行方向先说硬性红线第一微信用户资料头像、昵称、签名、位置轨迹属于个人信息未经授权批量采集涉嫌违规第二任何绕过微信客户端正常逻辑去读写用户数据的操作都违反平台运营规范第三即便你自建APP或小程序涉及收集地理位置也必须在隐私政策里明示目的、方式、范围并征得用户同意。在红线之内合规的“附近”功能开发方向其实很宽小程序内获取用户经纬度并存入自己的服务器、按距离展示“附近XX”列表、基于用户授权的位置做推荐排序、管理后台查看聚合成点的分布热力图不展示个人明细。这套东西做好之后技术上完全覆盖了“附近的人”的核心难点而且随时可以扩展成周边门店、打卡点、共享设备定位等任何商业化场景。这就是我把项目的重心从“采集”转向“位置服务”的原因。2. 核心思路与方案选型2.1 位置数据从哪来授权路径与坐标系合规的数据来源只有一个用户主动授权后通过小程序官方API获取当前定位。微信小程序里有两个关键接口wx.getLocation拿到当前设备的经纬度wx.chooseLocation让用户手动选择一个位置。后者更安全因为它把选择权交给了用户适合“选地点”而不是“强制追踪”的场景。这里有个很多新手踩坑的点微信返回的经纬度是GCJ-02坐标系国测局火星坐标不是GPS原始读出的WGS-84坐标系。国内地图厂商高德、腾讯都基于GCJ-02而直接从设备硬件或某些第三方SDK拿到的可能是WGS-84。如果混用会出现几百米甚至上千米的偏差这在“附近的人”场景里是致命的——1公里范围内可能偏差几条街。所以后端必须有一套统一的坐标处理逻辑先判断数据属于哪个坐标系再统一转换到业务使用的坐标系。具体判断和转换代码我会在第三节给出来。2.2 距离计算与近邻检索Haversine还是Redis Geo拿到坐标之后核心问题变成怎么高效地检索出“当前点附近N公里内”的目标最直接的做法是遍历所有POI点计算与当前点的距离筛选排序。小规模数据几千条完全没问题但到了几万、几十万条每次全表扫描就不现实了。我推荐两层方案组合使用第一层用Redis Geo做空间索引通过GEOSEARCH按半径捞出候选点这比遍历全表快几个数量级第二层候选点返回后用Haversine公式精确计算距离并排序因为Redis内部存的是简化过的geohash直接用它返回的距离字段做排序精度不够稳定。计算两点球面距离Haversine公式是标准答案。原理是基于球面三角把地球近似成半径6371公里的球体计算两点间的大圆距离。虽然地球实际是椭球体但在这个场景下误差在0.5%以内完全够用而且实现简单、计算快。公式如下a sin²(Δlat/2) cos(lat1) * cos(lat2) * sin²(Δlon/2) c 2 * atan2(√a, √(1−a)) d R * c对应代码我会在实操部分写一个函数直接用就好。2.3 整体架构怎么设计整个项目我拆成三个模块定位接入层、服务端计算层、展示层。定位接入层在小程序端负责获取用户经纬度并把GCJ-02坐标连同openid、时间戳一起POST到服务端。服务端计算层用Node.js或Python写接收坐标后用Redis Geo检索附近POI对候选点做Haversine精确距离计算再按距离排序返回列表。展示层就是一个简单的列表页左侧是距离信息右侧是POI名称和所属分类后端可另加一个管理页面显示聚合后的点位分布。这个设计的核心逻辑是“定位与业务分离”小程序只负责拿坐标和展示结果所有计算都在服务端完成。这样一方面避免在前端暴露全部POI数据另一方面后续换场景比如把POI换成门店、充电桩时服务端几乎不用改。3. 核心实操过程与代码实现3.1 后端坐标清洗与Haversine距离函数先写最基础的工具模块。我习惯直接用Python因为地理处理库多逻辑清晰。第一步是坐标系判断和转换因为前面说了微信拿到的GCJ-02和GPS原始WGS-84不能混着算。import math # 判断是否为GCJ-02坐标系粗略判断适用于国内范围 def is_gcj02(lat, lng): return 72.004 lng 137.8347 and 0.8293 lat 55.8271 # WGS-84 转 GCJ-02简化版足够日常使用 def wgs84_to_gcj02(lat, lng): a 6378245.0 ee 0.00669342162296594323 if is_gcj02(lat, lng): return lat, lng dlat _transform_lat(lng - 105.0, lat - 35.0) dlng _transform_lng(lng - 105.0, lat - 35.0) radlat lat / 180.0 * math.pi magic math.sin(radlat) magic 1 - ee * magic * magic sqrtmagic math.sqrt(magic) dlat (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * math.pi) dlng (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * math.pi) return lat dlat, lng dlng def _transform_lat(x, y): ret -100.0 2.0 * x 3.0 * y 0.2 * y * y 0.1 * x * y 0.2 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(y * math.pi) 40.0 * math.sin(y / 3.0 * math.pi)) * 2.0 / 3.0 ret (160.0 * math.sin(y / 12.0 * math.pi) 320 * math.sin(y * math.pi / 30.0)) * 2.0 / 3.0 return ret def _transform_lng(x, y): ret 300.0 x 2.0 * y 0.1 * x * x 0.1 * x * y 0.1 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(x * math.pi) 40.0 * math.sin(x / 3.0 * math.pi)) * 2.0 / 3.0 ret (150.0 * math.sin(x / 12.0 * math.pi) 300.0 * math.sin(x / 30.0 * math.pi)) * 2.0 / 3.0 return ret然后是Haversine距离计算函数这是所有检索功能的地基EARTH_RADIUS 6371.0 # 地球平均半径单位km def haversine(lat1, lng1, lat2, lng2): lat1, lng1, lat2, lng2 map(math.radians, [lat1, lng1, lat2, lng2]) dlat lat2 - lat1 dlng lng2 - lng1 a math.sin(dlat / 2) ** 2 math.cos(lat1) * math.cos(lat2) * math.sin(dlng / 2) ** 2 c 2 * math.asin(math.sqrt(a)) return EARTH_RADIUS * c * 1000 # 返回米这段代码有个细节math.radians一次性把四个参数全部从角度转弧度简洁但容易漏我在初版里就是分开转换导致结果偏差很大排了半天才发现是单位问题。建议注释写清楚返回值单位是“米”否则接口联调时很容易被前端质疑数据不准。3.2 存储层用Redis Geo做附近检索数据要支撑“附近N公里”的快速查询我直接用Redis的有序集合zset特性把经纬度编码成geohash后存入。Redis GEO指令本质上就是对zset的封装使用起来很顺手。# 添加一个POI点名称是poi:1001经纬度是116.397, 39.909 GEOADD nearby_pois 116.397 39.909 poi:1001 # 查询当前点(116.40, 39.91)周围5公里内的POI带上距离和坐标 GEOSEARCH nearby_pois FROMLONLAT 116.40 39.91 BYRADIUS 5 km ASC WITHCOORD WITHDIST用Python的redis-py联调时要注意GEOSEARCH在旧版本Redis6.2之前里叫GEORADIUS两个指令参数类似但新代码建议直接用GEOSEARCH语义更清晰。完整调用如下import redis r redis.Redis(hostlocalhost, port6379, db0) # 批量写入POI for poi in poi_list: r.geoadd(nearby_pois, (poi[lng], poi[lat], poi[id])) # 附近检索 results r.geosearch( nearby_pois, longitude116.40, latitude39.91, radius5, unitkm, sortASC, withcoordTrue, withdistanceTrue )这时候有个陷阱Redis返回的距离是近似值基于geohash单元格计算精度在某些边界区域不稳定。所以我在实际项目里只把Redis当作“圈人”的粗筛层圈出来后再用上一步的Haversine函数重算一遍真实距离保证最终排序和距离展示都是精确的。对几万级别的数据量这个方案的响应时间能稳定在10毫秒以内。3.3 小程序端定位授权与列表渲染小程序端的核心是获取位置然后调用后端接口。这里要注意两点一是wx.getLocation需要用户点击授权授权前最好先通过wx.getSetting检查授权状态避免一打开页面就弹窗被用户拒绝后很难重新引导二是拿到坐标后要立刻把 GCJ-02 的坐标系标识带上传给后端方便服务端判断是否需要转坐标。核心代码片段Page({ data: { nearbyList: [], loading: false }, onLoad() { this.checkAndGetLocation(); }, checkAndGetLocation() { wx.getSetting({ success: (res) { if (res.authSetting[scope.userLocation]) { this.getLocation(); } else { wx.authorize({ scope: scope.userLocation, success: () this.getLocation(), fail: () wx.showToast({ title: 需要位置授权才能查看附近, icon: none }) }); } } }); }, getLocation() { wx.getLocation({ type: gcj02, success: (res) { this.fetchNearby(res.latitude, res.longitude); }, fail: () wx.showToast({ title: 定位失败请检查手机定位, icon: none }) }); }, fetchNearby(latitude, longitude) { this.setData({ loading: true }); wx.request({ url: https://your-server.com/api/nearby, method: POST, data: { latitude, longitude, radius: 3 }, success: (res) { this.setData({ nearbyList: res.data.list }); }, complete: () this.setData({ loading: false }) }); } });这段代码里有几个细节值得展开type: gcj02这个参数很关键它决定了前端拿到的坐标坐标系选错的话和后端转换逻辑配合不上authorize失败后不要反复弹窗否则用户会在系统层直接关闭授权入口一定要引导用户去openSetting手动开启请求后端时把radius作为参数可以为后续做“1公里/3公里/5公里”的筛选切换预留空间。4. 常见问题与排查实录4.1 授权被拒后无法二次弹窗这是小程序开发里出现频率最高的问题。很多新人以为调wx.authorize被拒之后下次再调就能再弹窗实际上微信规则是用户一旦拒绝过授权后续调用wx.authorize不会再弹窗而是直接走fail回调。这时候唯一正确的处理方式是引导用户通过wx.openSetting手动打开设置页开启权限。排查思路写一个公共的定位授权工具函数每次进页面先getSetting看当前授权状态如果为false且用户之前拒绝过就弹一个自定义的引导弹窗按钮文案写“去开启”点击后调wx.openSetting。这个函数最好在app.js里挂到全局避免每个页面各写一遍逻辑。4.2 坐标偏移模拟器正常真机偏了三四百米项目上线前用开发者工具测试一切正常一上真机就发现POI点整体偏移了几百米。这个问题九成是坐标系混用导致的开发者工具默认返回的地图坐标参考系和真机GPS芯片原始输出不一致如果前端没有指定type: gcj02在某些Android机型上拿到的可能是WGS-84原始坐标直接拿来跟GCJ-02的POI数据做距离计算自然就偏差巨大。我建议排查路径是先在真机上用getLocation打点把原始返回值记下来再找一家地图开放平台例如腾讯或高德提供的坐标拾取器对比同一位置的坐标。如果经纬度在小数点后5位以上对不上基本就是坐标系没做转换。后端工整的处理是统一使用GCJ-02对入参坐标先判断再转换避免把坐标系判断的责任丢给前端。4.3 附近检索慢数据量一涨就跪初版为了省事我直接在MySQL里存了几万条POI附近查询用“经纬度加范围”的筛选加Haversine硬算数据量小没问题涨到几万后接口延迟直接飙到几百毫秒并发一上来数据库CPU立刻满载。最快的改造路径就是上Redis Geo把坐标数据写入Redis的热点key附近检索走GEOSEARCH通过ASC拿到按距离排序的候选集合再回MySQL按ID拉详情。这套改造完成之后相同的几万条数据接口延迟从差不多500毫秒降到了20毫秒左右。数据量到百万级别时还可以考虑Geohash分桶或引入专业的空间索引但对多数中小项目来说Redis Geo已经足够。4.4 隐私合规个人信息收集必须明示这个项目的合规问题比技术问题更重要。做“附近”功能时如果涉及的POI是真实个人用户点比如用户主动上报的位置必须在隐私声明里写清楚收集位置信息的目的用于展示附近内容、使用方式仅用于距离计算和排序、存储期限建议30天自动过期。同时产品上要提供“删除我的位置”入口用户在小程序内一个按钮就能清除自己的定位记录。这个点看起来是产品侧的但技术实现要在后端留好接口别等审核被驳回才补。5. 后续扩展与实际体会这套LBS方案跑通之后能扩展的方向其实很多。我后来在另一个项目里把它改造成了“附近共享充电宝”的查询服务后端数据结构几乎没动只把POI表的字段从用户模型换成了设备模型小程序端多加了几个图标的展示逻辑。还有一个朋友拿这套架构做园区内部的访客导航前端把地图换成canvas自绘简单示意图后端照旧是定位加附近检索一周就上了测试环境。另外有一个值得做的优化多级缓存。把热门商圈附近的POI查询结果缓存30秒能显著降低数据库压力再配合定时任务把POI数据预热到Redis冷启动时的延迟基本可以被吃掉。还有一个细节是距离排序后要处理“同距离”的稳定排序不然刷新列表时顺序会跳体验很差。我习惯在排序的第二个字段加上POI创建时间的倒序这样既能保证稳定又能让新录入的点有一定曝光。踩过几次坑之后我最大的体会是所谓“附近的人采集工具”真正值钱的部分不是“采集”而是“附近”——也就是地理空间索引、坐标转换、距离计算这一整套功夫。把这些基本功练扎实了微信生态里的合法位置服务场景随便换技术架构都能稳稳接住。希望这篇梳理能帮你避开那些坑少走几段弯路。本文还有配套的精品资源点击获取