地陪搭子系统开发实战:基于 SpringBoot + uniapp 的多端搭子匹配平台落地

「地陪搭子」本质上是一个 LBS 社交与预约服务结合的产品形态:一端是有本地陪同、城市讲解、摄影跟拍、徒步带路需求的用户,另一端是熟悉本地生活、可提供陪伴式服务的个人。技术实现上,它既要做「附近的人」这类地理位置匹配,又要处理预约单的状态流转、实时沟通与多端一致性。本文从架构、数据模型、匹配算法、多端适配四个维度,给出一套可复用的实现思路,技术栈参考同类多端项目常用的 SpringBoot + MyBatisPlus + MySQL + uniapp + Vue/ElementUI 组合。

一、需求拆解与技术选型

先把「地陪搭子」拆成四条主线需求:

  1. 身份与资质:实名认证、城市归属、技能标签(讲解、摄影、徒步、美食、方言等)。
  2. 匹配与推荐:按城市 + 距离 + 标签 + 评分召回,输出地陪搭子列表。
  3. 沟通与预约:IM 实时聊天、行程确认、订单状态流转、行程分享。
  4. 多端一致:安卓、iOS、小程序、H5、公众号共用一套业务能力。

对应的技术选型建议:

方案说明
用户端uniapp(Vue 语法)一套代码条件编译输出 5 端
管理后台Vue + ElementUI内容审核、标签维护、订单干预
服务端SpringBoot + MyBatisPlus快速 CRUD + 自定义复杂查询
存储MySQL 8 + RedisMySQL 存主数据,Redis 存 GEO 与热点
实时通信WebSocket(Spring 原生或 Netty)聊天、接单通知
文件对象存储 + CDN头像、行程照片、认证材料

多端项目容易踩的坑是「端上各写一套」,所以从天就要把业务逻辑收敛到服务端接口,端上只做渲染与权限申请。

二、核心数据模型设计

地陪搭子档案表是整个匹配链路的核心,关键字段包括城市、经纬度、服务半径、标签、接单状态与评分。

CREATE TABLE `dt_companion_profile` (
  `id`           bigint       NOT NULL AUTO_INCREMENT,
  `user_id`      bigint       NOT NULL COMMENT '用户ID',
  `city_code`    varchar(12)  NOT NULL COMMENT '城市编码,如 330100',
  `location`     point        NOT NULL SRID 4326 COMMENT '经纬度',
  `tags`         json         DEFAULT NULL COMMENT '讲解/摄影/徒步/美食',
  `languages`    varchar(64)  DEFAULT NULL COMMENT '可服务语种',
  `service_radius` int        DEFAULT 5000 COMMENT '接单半径(米)',
  `status`       tinyint      DEFAULT 0 COMMENT '0离线 1接单中 2忙碌',
  `score`        decimal(3,2) DEFAULT 5.00 COMMENT '综合评分',
  `response_rate` decimal(3,2) DEFAULT 1.00 COMMENT '响应率',
  `on_time_rate`  decimal(3,2) DEFAULT 1.00 COMMENT '准时率',
  `update_time`  datetime     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_city_status` (`city_code`, `status`),
  SPATIAL KEY `idx_location` (`location`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单表建议只保留「行程语义」字段:出发时间、时长、集合点、人数、备注、状态,不要把业务规则写死在表结构里。

CREATE TABLE `dt_order` (
  `id`          bigint      NOT NULL AUTO_INCREMENT,
  `order_no`    varchar(32) NOT NULL,
  `user_id`     bigint      NOT NULL COMMENT '下单用户',
  `companion_id` bigint     NOT NULL COMMENT '地陪搭子用户ID',
  `start_time`  datetime    NOT NULL,
  `duration_min` int        NOT NULL COMMENT '预计时长(分钟)',
  `meet_point`  varchar(255) DEFAULT NULL,
  `status`      varchar(16) NOT NULL DEFAULT 'CREATED',
  `create_time` datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_companion_status` (`companion_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

坐标统一是必须提前约定的事:国内地图 SDK 返回的是 GCJ-02,如果端上直接上报、服务端直接入库,就要保证写入与展示使用同一套坐标系,否则会出现几百米的偏移,在地陪搭子这种「集合点见面」的场景里是致命的。

三、地陪搭子匹配算法实现

匹配分两步:SQL 粗筛 + Java 精排。粗筛用空间索引和距离函数把候选集压到几百条,精排再用标签相似度和距离衰减做加权。

SELECT c.user_id,
       ST_Distance_Sphere(c.location,
            ST_SRID(POINT(#{lng}, #{lat}), 4326)) AS distance_m,
       (0.4 * c.score / 5
      + 0.3 * c.response_rate
      + 0.3 * c.on_time_rate) AS base_score
FROM dt_companion_profile c
WHERE c.city_code = #{cityCode}
  AND c.status = 1
  AND ST_Distance_Sphere(c.location,
        ST_SRID(POINT(#{lng}, #{lat}), 4326)) <= c.service_radius
ORDER BY base_score DESC, distance_m ASC
LIMIT 200;

精排部分用标签 Jaccard 相似度加上距离衰减:

public List<CompanionCardVO> rank(MatchQuery q, List<Candidate> candidates) {
    return candidates.stream()
        .map(c -> {
            double distanceDecay = 1 - Math.min(c.getDistanceM(),
                    q.getRadiusM()) / (double) q.getRadiusM();
            double tagSim = jaccard(q.getTags(), c.getTags());
            double finalScore = 0.5 * c.getBaseScore()
                              + 0.3 * tagSim
                              + 0.2 * distanceDecay;
            return new CompanionCardVO(c, finalScore);
        })
        .sorted(Comparator.comparingDouble(CompanionCardVO::getScore).reversed())
        .limit(50)
        .toList();
}

private double jaccard(Set<String> a, Set<String> b) {
    if (a.isEmpty() || b.isEmpty()) return 0d;
    Set<String> inter = new HashSet<>(a);
    inter.retainAll(b);
    Set<String> union = new HashSet<>(a);
    union.addAll(b);
    return inter.size() / (double) union.size();
}

工程上还有两点值得注意:

  • 离线批量刷新:响应率、准时率这类指标不要每次请求实时算,用定时任务每小时写回 dt_companion_profile
  • 缓存分层:把「城市 + 标签」维度的候选 ID 列表缓存在 Redis,用户请求时只做一次距离计算,能显著降低数据库压力。

四、实时沟通、订单流转与多端适配

实时沟通推荐用 WebSocket 长连接,消息先落库再推送,保证离线用户重连后能拉到历史消息。消息表按会话维度建索引,避免全表扫描。

订单状态机要收敛成枚举,禁止在业务代码里散落字符串判断:

public enum OrderStatus {
    CREATED,     // 已创建,待地陪搭子确认
    CONFIRMED,   // 已确认
    IN_SERVICE,  // 服务中
    FINISHED,    // 已完成,可评价
    CANCELED,    // 已取消
    REFUNDING    // 退款处理中
}

状态迁移建议用一张显式的迁移表来描述,例如 CREATED -> CONFIRMED / CANCELEDCONFIRMED -> IN_SERVICE / CANCELEDIN_SERVICE -> FINISHED。任何越级都直接抛异常,这样后续加签退、超时自动取消都很容易扩展。

多端适配主要靠 uniapp 的条件编译解决平台差异,尤其是定位与权限:

// #ifdef MP-WEIXIN
.getLocation({ type: 'gcj02', success: res => report(res) });
// #endif

// #ifdef APP-PLUS
uni.getLocation({
  type: 'gcj02',
  geocode: true,
  success: res => report(res)
});
// #endif

// #ifdef H5
// H5 需在 HTTPS 下调用浏览器定位,并做好降级为手动选点
// #endif

发布前务必在隐私协议中声明位置、相册、麦克风等权限用途,小程序端还要确认所选服务类目与功能一致,否则容易被驳回。

安全设计是这类产品必须前置考虑的部分:实名认证、行程分享给紧急联系人、敏感词过滤、评价双向可见、异常订单人工介入入口。这些能力都应放在服务端,端上只做入口。

五、部署与常见问题 FAQ

部署上,服务端打成 Docker 镜像,前面挂 Nginx 做 HTTPS 与静态资源托管;H5 与公众号共用一套构建产物,小程序单独发布。数据库开启慢查询日志,空间查询建议单独压测,确认空间索引真正生效(EXPLAIN 中应出现 SPATIAL 相关索引)。

Q1:地陪搭子系统一般用什么技术栈?
常见组合是 SpringBoot + MyBatisPlus + MySQL 做服务端,uniapp 做用户端并编译到安卓、iOS、小程序、H5、公众号,管理后台用 Vue + ElementUI。这套组合的优势是一套业务逻辑覆盖多端,运维成本低。

Logo

一站式 AI 云服务平台

更多推荐