跳到主要内容

用 Claude Code + 腾讯云开发 CloudBase 5 天上线一个 LBS 群聊小程序:技术实录

CloudBase TeamCloudBase Team
阅读需 11 分钟

五一假期 5 天,我做了一个微信小程序——叫《会稽群聊》。

需求一句话:游客走到绍兴某个景点,扫码后能围观那个景点对应的本地古人在微信群里吵架。 鲁迅故里入口解锁鲁迅家族群、沈园解锁陆游唐婉群、兰亭解锁王羲之群——总共 6 个群。

这篇文章只讲怎么做出来的——剧本数据结构、围观模式状态机、LBS 半径判定、AI 调用、Day-by-Day 节奏、踩过的坑。idea 怎么来的、为什么是绍兴这些不展开。

技术栈三件套

选择为什么
前端微信小程序LBS 权限走原生最丝滑,免下载,审核比 App 快
后端腾讯云开发 CloudBase(云函数 + NoSQL + 云存储)一体化,不用自己运维
AICloudBase 内置 Hunyuan / DeepSeek云函数里 cloud.extend.AI 直接调,零对接
开发工具微信开发者工具(内置 AI 编程,后文简称 IDE)+ Claude Code写小程序本体在 IDE,写剧本和云函数在 Claude Code

为什么不用 Next.js + Supabase + Vercel:海外栈在国内有延迟、备案、微信生态接入三道坎,5 天根本跑不通。详细对比之前那篇《2026 一人公司技术架构》写过,这里不展开。

数据结构:把"群聊"变成数据库文档

整个产品的数据模型只有 4 个集合:

// 1. groups: 6 个群的元数据
{
_id: "lu-xun-family",
name: "鲁迅家族のChat",
spotId: "luxun-guli", // 关联打卡景点
avatarUrl: "...",
memberIds: ["lu-xun", "zhou-zuoren", "zhu-an", "xu-guangping", "run-tu"],
unlockOrder: 1 // 默认开放给所有用户
}

// 2. members: 所有人物角色卡
{
_id: "lu-xun",
displayName: "鲁迅",
age: 48,
personality: "刻薄敏感爱反讽,写作时抽烟",
avatarUrl: "...",
artifactNotes: ["《呐喊》", "线装日记本"] // 关联可点击的文物
}

// 3. scripts: 群聊剧本(每群 8-15 条,分段解锁)
{
_id: "lu-xun-family-act1",
groupId: "lu-xun-family",
unlockSpotId: "luxun-guli-entrance", // 在哪个 LBS 点解锁
messages: [
{ from: "zhou-zuoren", text: "大哥,请你以后不要再到后边院子里来。", type: "text" },
{ from: "zhou-zuoren", text: "[图片]", type: "image", imageUrl: "..." },
{ from: "lu-xun", text: "[已撤回一条消息]", type: "system" },
{ from: "zhu-an", text: "大先生,今天的茴香豆刚买好。", type: "text" },
// ...
]
}

// 4. user_progress: 用户解锁进度
{
_id: "openid-xxx",
unlockedScripts: ["lu-xun-family-act1", "shen-yuan-act1"],
stamps: ["luxun-guli", "shen-yuan"], // 集章
reactions: { "lu-xun-family-act1": ["🍶", "🥢"] }
}

为什么用 NoSQL 不用 MySQL:剧本结构嵌套深、字段不固定(图片消息和文本消息字段不一样),关系型表会很丑。CloudBase 的云数据库支持嵌套查询,写起来直接:

// 云函数里取一个群所有已解锁的剧本
const db = cloud.database();
const scripts = await db.collection('scripts')
.where({
groupId: 'lu-xun-family',
unlockSpotId: db.command.in(userProgress.stamps)
})
.orderBy('createdAt', 'asc')
.get();

LBS 半径判定:100m 解锁

这是核心交互——用户必须人到了景点,才能解锁下一段对话

前端拿定位,后端验距离。两段必须都在,前端单独算很容易被改。

// 小程序 wx.getLocation
wx.getLocation({
type: 'gcj02', // 国测局坐标系,微信小程序原生
isHighAccuracy: true, // 高精度模式,5-10m
success: (res) => {
wx.cloud.callFunction({
name: 'unlockSpot',
data: {
spotId: 'luxun-guli-entrance',
userLat: res.latitude,
userLng: res.longitude
}
});
}
});

云函数里 Haversine 算距离(地球曲面真实距离,不是直线):

// cloudfunctions/unlockSpot/index.js
const cloud = require('wx-server-sdk');
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });

const SPOTS = {
'luxun-guli-entrance': { lat: 30.0021, lng: 120.5797, radius: 100 },
'shen-yuan': { lat: 29.9924, lng: 120.5871, radius: 100 },
'lan-ting': { lat: 29.9583, lng: 120.5419, radius: 150 }, // 兰亭范围大,半径放大
// ...
};

function haversine(lat1, lng1, lat2, lng2) {
const R = 6371000; // 地球半径,米
const toRad = (x) => x * Math.PI / 180;
const dLat = toRad(lat2 - lat1);
const dLng = toRad(lng2 - lng1);
const a = Math.sin(dLat/2) ** 2 +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng/2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

exports.main = async (event) => {
const { spotId, userLat, userLng } = event;
const spot = SPOTS[spotId];
if (!spot) return { ok: false, reason: 'unknown_spot' };

const distance = haversine(userLat, userLng, spot.lat, spot.lng);
if (distance > spot.radius) {
return { ok: false, reason: 'too_far', distance: Math.round(distance) };
}

const { OPENID } = cloud.getWXContext();
const db = cloud.database();
await db.collection('user_progress').doc(OPENID).update({
data: {
stamps: db.command.addToSet(spotId),
[`unlockedScripts`]: db.command.addToSet(`${spotId}-act1`)
}
});
return { ok: true };
};

踩过的坑

  1. 微信小程序坐标系是 gcj02(国测局加密),不是常见的 wgs84(GPS 原始)。一开始用百度地图查的景点经纬度算出来全部偏移 50-300m,全部解锁失败
  2. 山区景点 GPS 飘移大——兰亭周围有山,半径放到 150m 才稳定
  3. 苹果手机定位精度高于安卓——isHighAccuracy: true 在低端安卓上偶尔超时(默认 3 秒),加 highAccuracyExpireTime: 5000 兜底

围观模式:用户默认不能说话

整个产品最反直觉的一个决策——用户在群里只能发表情、投票、点击文物名,不能直接打字。

技术上是用一个简单的状态机控制:

// 前端 page/chat/chat.js
data: {
inputMode: 'spectator', // 'spectator' | 'reaction' | 'vote'
allowedReactions: ['🍶', '🥢', '🪶', '🌧️', '🐢', '🪨'] // 绍兴限定六件套
},

// 用户点击底部输入区
onTapInput() {
if (this.data.inputMode === 'spectator') {
wx.showToast({
title: '默认围观哟,发表情吧',
icon: 'none'
});
return; // 不弹键盘
}
}

为什么这么设计:

  • 避免 AI 客服感——一旦用户能直接和 AI 角色对话,体验就退化成 ChatGPT 套壳
  • 保护剧本完整性——预设剧本是经过史料考证 + 戏剧编排的,用户插话会破坏节奏
  • 降低 AI 成本——所有对话都是离线生成、存在数据库里,运行时几乎不再调 AI(除了反应统计),单用户成本控制在 0.001 元以下

反应表情是有后端聚合的:

// cloudfunctions/sendReaction/index.js
exports.main = async (event) => {
const { scriptId, emoji } = event;
const db = cloud.database();
await db.collection('reactions').add({
data: { scriptId, emoji, createdAt: db.serverDate() }
});
// 同步给所有看这条剧本的人(实时推送用 CloudBase Realtime)
return { ok: true };
};

CloudBase 的实时数据推送(Realtime)让"围观"有了集体氛围感——你在沈园看陆游和唐婉那段,能看到 30 秒前另一个游客刚发的破防 emoji 飞过。这是单机玩不出来的爽点。

CloudBase 内置 AI 写剧本

剧本不是手写的,是 AI 生成的——但所有 AI 调用都在线下完成,运行时不调

云函数里调 Hunyuan:

// cloudfunctions/generateScript/index.js
const cloud = require('wx-server-sdk');
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });

exports.main = async (event) => {
const { groupId, conflictPrompt } = event;

const ai = cloud.extend.AI;
const result = await ai.generateText({
model: 'hunyuan-2.0-instruct-20251111',
messages: [
{
role: 'system',
content: `你是历史群聊编剧。按"微信群对话"格式输出 10-15 条消息,
每条不超过 30 字,必须包含 [图片] [已撤回] [正在输入] 等微信原生标记,
节奏要有沉默和留白。严格按时间线,不能让人物说出他死后才出现的话。`
},
{ role: 'user', content: conflictPrompt }
]
});
return { messages: parseMessages(result.text) };
};

调用时的 prompt 是「人物性格卡 + 冲突核心 + 文物锚点」三件套,举例:

【人物性格卡】
鲁迅:48 岁,刻薄敏感爱反讽
周作人:43 岁,温和记仇字斟句酌
朱安:49 岁,文盲,绍兴话
许广平:35 岁,新女性,住外面
闰土:50 岁,乡下渔民,木讷

【冲突核心】
1923 年 7 月 19 日早晨,周作人递给鲁迅一封绝交信

【文物锚点】(必须出现)
- 八道湾胡同 11 号后院门
- 鲁迅日记本(线装)
- 朱安从绍兴带的茴香豆

【输出】
微信群对话 10-15 条,要有节奏,要有沉默

我做的事就两件:

  1. 校时间线——AI 经常让 1923 年的鲁迅说 1936 年才出现的话,必须人工兜底
  2. 加方言——绍兴话彩蛋(朱安那句"我是大先生的太太")AI 写不出来

成本:6 个群一共 12 段对话(每群 2 段),Hunyuan 总调用约 30 次(含调试),加起来不到 1 元。

Day-by-Day:5 天怎么排的

日期任务工具实际耗时
4/30数据建模 + 6 群人物清单 + 6 段冲突大纲飞书文档4h
5/1 Day1小程序前端骨架(聊天 UI + 进度页 + 地图页)微信开发者工具 + 内置 AI8h
5/2 Day26 群剧本生成 + 校对Hunyuan + 人工6h
5/3 Day34 个云函数(unlockSpot / sendReaction / getProgress / sealAchievement)Claude Code + CloudBase MCP5h
5/4 Day4LBS 接入 + 文物弹层 + 打卡相册云存储 + wx.getLocation7h
5/5 Day5联调 + 修 bug + 提交审核真机扫景点(杭州→绍兴→杭州)9h

总计约 39 小时纯开发。两件事让节奏跑得通:

1. 用 CloudBase MCP 让 Claude Code 直接管后端

~/.workbuddy/mcp.json 里挂 @cloudbase/cloudbase-mcp@latest,Claude Code 能直接:

  • 创建云函数
  • 改云函数代码 + 部署
  • 查云数据库内容
  • 上传文件到云存储

写云函数的 prompt 直接是:"给我写个 unlockSpot 函数,输入 spotId/userLat/userLng,用 Haversine 算距离,距离小于 100m 就把对应剧本加入用户解锁列表"。Claude Code 写完直接调 MCP 部署上去,不用复制粘贴。

2. 微信开发者工具内置的 AI 编程能力,写小程序前端比 Cursor 顺

聊天 UI 那种气泡布局、列表渲染、滚动到底部,原本是最磨叽的,IDE 内置的 AI 一句"做一个微信群聊样式的对话列表"直接生成可用的 wxml + wxss。这个之前那篇《微信开发者工具+AI,从写代码到部署上线没离开过这个窗口》详细写过工作流。

几个性能/成本数字

实测
单用户首屏冷启动800ms(含云函数冷启动)
LBS 解锁单次延迟平均 320ms(GPS 200ms + 云函数 120ms)
单用户全程访问云函数次数~25 次(6 个解锁 + 6 个集章 + 反应/进度若干)
6 个群剧本总字数约 4500 字 + 30 张文物图
云存储占用12MB(图片)+ 1MB(数据库)
日活 1000 人预估月成本约 8 元(CloudBase 免费额度内基本不用付费)
微信小程序审核耗时提交到通过 18 小时(无内容驳回)

最后一行是惊喜——鲁迅、王阳明、徐渭这种近代/近古名人 AI 生成对话居然过审了。猜测是因为对话节选都在公开史料范围内,且人物已离世足够久。但不建议复制时加在世人物或政治敏感人物,那是另一回事。

踩过的几个坑

  1. gcj02 vs wgs84 坐标系——前面讲过,景点经纬度必须用国测局加密后的 gcj02,否则全部解锁失败
  2. CloudBase Realtime 推送数量限制——免费额度是 100 个并发连接,超出就要付费。MVP 阶段够用,上量需要自己算账
  3. AI 生成的"图片"占位符——AI 输出 [图片] 后我得手动配图,否则前端渲染就是个空块。后续考虑接 CloudBase 内置文生图模型自动配
  4. 审核敏感词预检——提交前用微信官方的内容安全 API(security.msgSecCheck)扫一遍所有剧本文本,避免"色情""暴力"等词触发驳回。Claude Code 一键写脚本批量扫
  5. iOS 静默定位限制——小程序在后台不能持续拿定位,必须用户主动点"扫码解锁"按钮触发一次定位,不能做"自动检测到达"

最后

整个工程量不是创意而是工具链成熟度

5 天能做完,是因为:

  • CloudBase 把前端、后端、数据库、AI、存储收成一个 SDK
  • 微信开发者工具把小程序写代码 + 调试 + 预览 + 上传收成一个窗口
  • Claude Code + MCP 把"想"和"做"之间的搬运成本压到接近零

三年前同样的需求,光搞定登录态 + LBS 权限 + 推送鉴权就要两天。现在你的瓶颈从"工具会不会用"变成了"想做什么"。

仓库地址正在脱敏,下周公开。小程序码在评论区。

用 CloudBase 构建下一款应用

一站式后端云服务,覆盖数据库、云函数、静态托管与 AI 能力。