刚把这个“博物馆小程序系统”从头到尾捋了一遍,后端用的 Spring Boot,管理后台用的 Vue,游客端是微信小程序。这类项目现在有一个统一的名字叫“智慧文旅”或“一机游”,但真正做过的人都知道,名字越大,细节越碎。项目标题里带了“128”,我理解成内部立项或者迭代代号,和什么性能指标没关系,很多文旅项目都习惯用版本号或者场馆编号来命名,反正叫到最后大家只知道这是一个“博物馆小程序系统”。
我从项目整体拆解、技术选型、功能模块、后端构建、小程序端开发、Vue 管理后台、上线排障这几个维度,把整套东西说清楚。里面所有步骤和参数都是实际项目里会遇到的级别,不是“跑通 Demo”那种。
1. 这个项目到底是什么,解决的是哪个层面的问题
先说结论:这本质不是一个“小程序开发”项目,而是一个“博物馆数字化运营”项目。小程序只是游客最容易触达的那一层,真正复杂的是它背后的控制台、内容管理、票务库存、支付回调、数据统计这一大串链路。
1.1 对标题里“博物馆小程序系统”的重新理解
大部分做开发的人,第一次看到这类标题会下意识地拆成两个东西:给游客用的微信小程序,给管理员用的后台系统。但实际业务里至少有三类用户,每种用户对系统的诉求完全不一样:
- 游客:查预约、买票、看导览、听讲解、逛文创店、看展讯。
- 场馆运营人员:维护展品信息、管理票务库存、监控实时客流、处理退款。
- 管理层/文旅集团:看数据报表、做营销活动、管理会员、分析来源渠道。
所以系统拆开以后是三个端:C 端小程序、B 端管理后台、开放接口层。标题里只写了“springboot+vue+微信小程序”,但实际上还要加一层对象存储、一层消息缓存、一层定时报表,不然功能很难闭环。
1.2 我梳理这个项目时先抓的几条主线
我拿到这种标题,第一反应不是列页面,而是先抓几条业务流程线。
第一条是门票预约线。大型博物馆基本都要预约,尤其是热门场馆。这条线涉及场次、库存、锁单、支付、退款、入馆核销。最容易出问题的就是并发抢票,同一时刻很多人点“提交预约”,库存一超卖就是投诉。
第二条是内容服务线。博物馆的核心价值在线下,但线上要用图文、语音、视频、3D 模型来呈现展品。这条线涉及内容模型设计、管理系统上传、C 端展示、CDN 分发。想做得好,还得考虑“内容运营没有技术背景”这个现实问题。
第三条是会员营销线。博物馆现在很重视私域流量,小程序天然适合做会员注册、积分任务、文创商城、优惠券发放。这里接口不一定复杂,但数据一致性要求高,比如积分扣了但订单没生成,用户会立刻察觉到。
第四条是数据统计线。场馆想知道哪些展区热、哪些讲解没人听、哪些时段客流集中。小程序上报行为数据,后台出图表,运营再反推改进。
这四条主线,单拿出来都不难,难的是拼在一起以后权限、缓存、消息、回调全都搅在一起。所以我的建议是,项目一开始就要有清晰的模块边界,不能写成一个巨大的 controller 堆起来的东西,哪怕团队只有两三个人。
2. 技术选型:为什么是 Spring Boot、Vue 和微信小程序这三件套
很多人在技术群里争论“博物馆小程序该用 uni-app 还是原生”“后端该用 Node 还是 Java”。我的看法很直接:技术选型不取决于谁的语言最酷,取决于项目生命周期里谁最不容易翻车。
2.1 后端用 Spring Boot,不是因为它新,而是因为它成熟
这类文旅项目一旦上线,就要长期运行,每年都有节假日大流量,还可能对接文旅监管平台、支付渠道、短信服务商。Java 系在这里的优势是稳定、事务能力强、能打的中间件多。
具体选 Spring Boot,原因有三个。
第一,Spring Boot 的生态对“单体应用 + 后续拆微服务”的现实路径支持最好。大多数博物馆项目并发量没到需要微服务的程度,一个单体应用开几个实例就足够。Spring Boot 用 Maven/Gradle 管理模块,后面就算拆成用户服务、订单服务、内容服务,边界也比较自然。
第二,事务和 AOP 是刚需。门票预约里“扣库存 + 生成订单 + 扣积分”必须在一个事务里;打日志、幂等控制、防重复提交这些横切逻辑,用 AOP 注解做比在每个接口里手动写要省太多事。
第三,招聘和接手成本。Spring Boot 在国内有大量存量开发者,团队里随便拉一个后端都能看懂,不至于一个人离职系统就废掉。
这里不是完全否定 Node.js 或者 Go,如果项目只是做一个展示型小程序,Node 完全够了。但一旦涉及票务、支付、库存、对账,Java 系在事务一致性和运维生态上确实让我更放心。
2.2 管理后台用 Vue,是当前最稳妥的选择
后台管理系统的需求是页面多、表单多、权限复杂,对沉浸式体验要求不高。Vue 在国内的前端团队里普及率极高,Element Plus 这类现成的中后台组件库基本能覆盖 80% 的后台页面需求。
有人问是不是应该用 React,我的回答是:如果你的团队里根本没有 React 熟手,没有必要为了“大厂都在用”去强行切换。Vue 的模板语法对从后端转前端的同事更友好,v-if、v-for、computed 这些概念比较容易理解。实际项目里管理后台经常是后端同事来维护的,Vue 的入门成本在这里就是生产力。
另一个考量是,Vue 的单文件组件结构,跟小程序的分包结构在思维方式上非常接近。都是 template/script/style,虽然运行时不是一回事,但团队切换心智负担小。
2.3 微信小程序作为 C 端载体,是这个项目最合理的一步棋
博物馆游客的典型路径是这样的:朋友分享链接、公众号看到文章、短视频刷到视频 -> 识别小程序码 -> 直接打开预约页。这个路径里,用户最不想做的一件事情就是下载 App。
微信小程序解决了“低频 + 强需求”场景的矛盾。游客一年可能只去一次博物馆,让他装一个 App 留存根本不可能。小程序用完即走,但沉淀了会员身份,下一次推活动他还能收到订阅消息。支付、登录、分享、地理位置都能直接调微信的接口,不需要自己造轮子。
我特别想提醒一点:不要因为某些 UI 框架宣传“多端复用”就忽视微信小程序原生能力。蓝牙信标扫描、AR 能力、硬件传感器调用,这些和具体平台绑得很深。如果一开始就套了一个跨端框架,后面做实时客流、室内定位的时候,反而会因为框架抽象层太厚导致无法拿到原始能力,很别扭。
| 技术选型 | 推荐方案 | 为什么 |
|---|---|---|
| 后端 | Spring Boot 2.x/3.x | 生态成熟,事务/AOP/中间件支持好,单体即可承载 |
| 管理后台 | Vue3 + Element Plus | 团队上手快,组件丰富,适合表单和权限管理 |
| 游客端 | 微信小程序原生 | 调用微信登录支付最直接,蓝牙/位置权限可控 |
| 数据库 | MySQL 8.x | 关系型设计稳定,管理系统和订单系统都依赖 |
| 缓存 | Redis | 库存预扣、分布式锁、热点数据缓存都靠它 |
| 对象存储 | 腾讯云 COS / 阿里云 OSS | 图片、音频、3D 模型都需要 CDN 分发,这个是刚需 |
3. 功能模块拆解,先画清楚再写代码
很多项目最后烂尾,不是因为某个页面做不出来,而是因为功能需求没拆干净,导致接口改了又改。我习惯先把模块画清楚,再进入“后端骨架”这个章节。
3.1 票务预约与库存扣减:防超卖比做界面难十倍
博物馆门票预约的流程:选择日期 -> 选择场次 -> 选择数量 -> 填写游客信息 -> 支付 -> 生成二维码/预约凭证。核心难点在“库存”两个字。
我把库存拆成两层。
底层是数据库可售余票。每次下单前要扣减,扣减语句必须带上条件,不能先查再改。具体来说就是这样的更新语句:
UPDATE ticket_stock SET remaining = remaining - #{buyNum} WHERE session_id = #{sessionId} AND remaining >= #{buyNum}这条语句利用数据库的乐观条件判断,确保不会超卖。如果影响行数是 0,就说明库存不够,直接返回“已售罄”。这里的关键是:不要把查询到的库存放到内存里再计算,必须让数据库来做原子更新。
上层是 Redis 预扣缓存。高峰期为了降低数据库压力,会先把库存加载到 Redis,用 Lua 脚本做原子扣减。比较常用的脚本格式如下:
local stock = tonumber(redis.call('get', KEYS[1]) or '0') local amount = tonumber(ARGV[1]) if stock >= amount then redis.call('decrby', KEYS[1], amount) return 1 end return 0用 Lua 是为了原子性。如果不做原子操作,两个请求同时读到库存都是 1,都会认为自己扣成功了,这就又超卖了。
票务还有一个必须处理的逻辑是“超时未支付释放库存”。比如用户锁定了 2 张票,但 15 分钟没有支付,系统需要把库存加回来。这个逻辑不能简单用定时任务扫全表,最好在生成订单时把订单号写入 Redis 延迟队列,或者用消息中间件的延迟消息,到时间后检测订单状态,如果还是“待支付”就关单并回补库存。
做这块的时候一定要区分“锁库存”和“扣库存”的区别。锁库存是临时占住,支付成功才真正扣减;未支付释放库存是补偿。很多问题的根源是模型没分清楚。
3.2 数字导览与内容管理:AR 只是锦上添花,内容才是主角
博物馆做数字导览的几种常见形式:文字介绍、语音讲解、视频解读、AR 叠加、3D 模型。技术上前三种最简单,AR 和 3D 模型最炫,但实际维护成本也最高。
我的建议是内容系统不要一上来就做 AR。先把基础模型设计成“展品 + 多媒体素材列表”,一个展品关联多张图片、多个音频、多个视频。内容管理系统里让运营人员可以随时增加、替换、上下架素材。这样即便没有 AR,游客体验也已经在线。
做 AR 或 3D 模型时,有几个参数必须提前想好:
- 3D 模型格式。小程序端比较建议使用 GLB 格式,这是 GLTF 的二进制版本,体积小、解析快。原始文物扫描模型动辄几百 MB,不压缩根本没法在手机上跑。
- 压缩方案。模型需要做减面、纹理压缩,一般单模型控制在 20MB 以内,否则用户流量和加载耗时会非常难看。
- CDN 分发。音频、模型、图片全部走对象存储 + CDN,重点是要在 URL 上带版本号或 hash,不然内容更新后客户端缓存还是旧文件。
展品内容的核心不是技术,而是“运营字段”足够灵活。比如同一件展品,不同游客可能需要不同深度的讲解:入门版 30 秒,深度版 3 分钟。内容模型要把这些做成字段,而不是写死在界面里。
3.3 实时客流与展馆热力:很多团队低估的硬骨头
实时客流在博物馆场景里不是“显示一个数字”这么简单。最常用的方案是基于蓝牙信标或者 Wi-Fi 探针,游客只要打开小程序并授权蓝牙权限,手机就会周期扫描到附近的信标,上报给后端计算出位置。
这块的坑主要在信号不稳定。人在走、手机在兜里、蓝牙信号会被遮挡,所以上报的坐标经常跳来跳去。技术上要做到两点:
- 滑动窗口去重。同一个设备上报的数据,在 5 秒内只处理一次,避免一台手机制造出一百个假数据。
- 信号强度过滤。只保留最近 5 秒内 RSSI 最强的几个信标,再根据三角定位或者区域划分确定游客在哪一块展区。
客流热力图对后端是有实时写压力的。建议用消息队列接收原始上报,后端异步消费,落库汇总成分钟级数据,再渲染到管理后台的地图上。不要每次上报都直接写业务表,会扛不住。
另外,如果要做“当前等待时长”这类预测功能,需要结合历史客流、当前排队人数、人均参观时长一起算。这里没有太复杂的算法,先用简单的移动平均就能跑起来,不需要一开始上人工智能模型。
3.4 会员体系与文创商城:小心支付回调不幂等
博物馆的文创商城,基本是“藏品数有限、下单量不大、但客单价高”的模式。技术难度不高,但有几个数据一致性的细节。
会员积分要么跟小程序登录绑定,要么跟手机号绑定。游客通过微信授权登录后,系统拿到 openid,再在会员表里生成一份用户档案。如果用户在小程序里填了手机号或绑定身份证,就升级为实名会员。
文创商品下单和门票预约一样,走“商品库存 + 订单 + 支付”流程。支付完成之后,微信会异步回调后端通知“支付成功”。这个回调默认可能重试多次,如果后端对回调不做幂等处理,就会出现一个订单被重复更新、积分被重复加、库存被重复扣的情况。
我处理支付回调的思路是:为每个回调生成一个唯一的通知唯一 ID,数据库表里加唯一索引。每次处理回调前先插入一条“回调记录”,如果插入冲突,说明已经处理过,直接返回成功,不再执行业务逻辑。这样就算微信重复通知十次,也不会破坏数据。
4. Spring Boot 后端骨架到底怎么搭
画完模块图,接下来就是把骨架搭起来。
4.1 项目分层和目录结构
我习惯把工程按职责分成几个 Maven 模块,而不是按技术分层去建包。虽然标题只写了 Spring Boot,但实际管理端接口和小程序端接口建议分开,因为接口鉴权方式不同:
museum-system ├── museum-common // 统一返回体、异常、常量、工具类 ├── museum-admin // 管理后台接口,登录走后台用户账号密码 ├── museum-app // 小程序接口,登录走微信 code2session ├── museum-job // 定时任务:订单超时关单、报表统计 └── museum-openapi // 支付回调、第三方对接的开放接口为什么这么分?因为小程序端和管理后台的认证模型不一样。管理后台是用户密码登录,小程序端是微信 openid 登录,如果混在一个模块里,拦截器写起来就很别扭。分开之后,两个模块各自维护自己的鉴权过滤器,公共能力下沉到 common 里,后面测试也好写。
Controller 层只做参数校验和返回统一响应体,Service 层做业务逻辑,Mapper 层只做数据库访问。这个分层看着老套,但维护的时候是真的香。
4.2 小程序登录认证和 JWT 的使用逻辑
小程序端每一次请求都要知道“你是谁”。流程是:
- 小程序调用
wx.login(),拿到临时 code。 - 小程序把 code 发给后端。
- 后端拿着 code + 小程序的 appid + secret,调用微信接口获取 openid 和 session_key。
- 后端用 openid 查用户表,没有就自动创建。
- 后端生成一个 JWT 返回给小程序,小程序存到本地 Storage。
- 后续请求都带上这个 JWT,后端解析出 userId。
这里有一个经常出问题的点:session_key绝对不能返回到小程序端。有些开发者图方便,直接把微信返回的 session_key 传给前端,后面要解密手机号或用户信息时再拿回来用。这很危险,session_key 是临时会话凭证,一旦泄露,相当于把你的登录凭证交出去了。
JWT 的有效期建议设短一点,比如 2 小时,另外再提供一个 refresh_token,客户端在 token 快过期时自动刷新。这样避免用户长时间挂在系统里,也避免每次启动小程序都要重新 wx.login。
4.3 数据库表设计要注意的几个点
博物馆系统相对其他业务系统来说,表结构并不算复杂。核心表大概有这些:
- 用户表:保存游客/后台用户。
- 场馆表:保存场馆基础信息。
- 场次表:保存开放日期、时段、总库存。
- 库存表:按天或按时段记录余票。
- 订单表:保存订单主信息。
- 订单明细表:保存每张票/每件商品的具体信息。
- 展品表:保存文物展品基本信息。
- 内容表:保存展品关联的图文、音频、视频、模型。
- 信标表:保存蓝牙信标位置。
- 积分流水表:保存积分增减记录。
- 支付回调记录表:保存所有支付回调,用于幂等。
比较关键的几个设计点是:
第一,库存表不能只放一个“总库存”字段。热门博物馆是有场次的,每个场次每天的库存和余票都不同,所以库存表要有日期和场次维度,并且要加唯一索引(session_id, biz_date)。
第二,订单号必须全局唯一。建议不要用数据库自增 ID 当订单号,而是生成类似“时间戳 + 随机数 + 渠道前缀”的长订单号。这样方便后续对账,也避免被猜到总量。
第三,订单状态不要用枚举串,直接用数字。订单表里status字段用0待支付 1已支付 2已取消 3已退款 4已核销,状态流转收敛在 Service 层控制。线上排查问题的时候,数字比字符串好查。
4.4 幂等与限流的具体实现
在项目里,后端接口除了业务逻辑,还要加上幂等控制和限流,这两点是“生产可用”和“演示 Demo”的分界线。
幂等控制的思路比较简单。在 Controller 层加一个@RepeatSubmit注解,实现方式是用 AOP 切面,以请求用户 ID + 接口路径 + 请求参数摘要做 key,写入 Redis,设置几秒的过期时间。如果同一个 key 已经存在,就拦截掉这次请求,直接返回“请勿重复提交”。
代码大致是这样:
@Aspect @Component public class RepeatSubmitAspect { @Around("@annotation(repeatSubmit)") public Object around(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable { // 获取当前用户ID + 请求签名 String key = buildKey(pjp); Boolean first = redisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofSeconds(repeatSubmit.interval())); if (!Boolean.TRUE.equals(first)) { throw new BizException("请勿重复提交"); } return pjp.proceed(); } }限流可以用 Redis 的令牌桶或者滑动窗口实现。最简单的方式是对特定接口做 IP 或用户维度的频率限制,比如“发送验证码接口,一个用户 60 秒只能发一次”。不管是令牌桶还是计数器,核心是限流必须作用在接口入口,而不是散落在业务代码里。
5. 微信小程序端从页面到接口的完整链路
后端搭好以后,小程序端的工作量主要集中在这几块:页面结构、登录态管理、接口封装、性能体验。
5.1 目录分包、tabBar 和核心页面
微信小程序有主包大小限制,而且主包越大,冷启动越慢。我把项目结构做成这样:
pages/ ├── index/ // 首页 tab ├── museum/ // 展馆导览 tab ├── mall/ // 文创商城 tab ├── user/ // 个人中心 tab ├── order/ // 订单流程分包 └── article/ // 展品详情分包主包只放 tabBar 页面和公共组件,其他功能尽量丢到分包里。比如订单确认、支付结果、退款详情这些页面都不是高频页面,完全适合做分包。分包体积控制在 2MB 以内是比较稳的,超过的话 iOS 和 Android 的加载体验都会变差。
tabBar 我一般建议四项:首页、展馆、文创、我的。不要放第五个“活动”之类的,操作复杂度一高,游客就不会用了。
5.2 登录态管理和 token 刷新
小程序端最常见的错误是每次都调 wx.login,然后重新拿 code 换 token。正确逻辑是:
- 第一次启动:调 wx.login,换 token,存 Storage。
- 后续启动:先看本地 token 是否存在,存在就先用,等到接口返回 401 再刷新。
- 刷新逻辑:用 refresh_token 调刷新接口,拿到新 token 后重放失败的请求。
接口封装层用 Promise 统一处理。所有请求都带一个 header:
const request = (url, data = {}) => { const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.request({ url: baseUrl + url, method: 'POST', data, header: { Authorization: `Bearer ${token}` }, success: (res) => { if (res.data.code === 401) { // 刷新 token 后重放,这里要注意避免死循环 refreshTokenAndRetry(url, data, resolve, reject); } else if (res.data.code === 200) { resolve(res.data.data); } else { reject(res.data); } }, fail: reject, }); }); };刷新 token 的并发问题也要处理:不能同时触发多次刷新请求,否则容易把 refresh_token 刷没。用一个全局的 Promise 缓存刷新动作,第一个请求在刷,后面的请求等同一个 Promise 完成再重放。
5.3 交付前的页面性能与体验优化
小程序在博物馆场景里使用,往往是游客已经站在太阳底下排队时才打开。加载慢、白屏、图片转圈,都是差评来源。
我的经验是首屏不能依赖太多接口。首页上来最多并发请求四个接口:轮播图、预约状态、推荐展品、公告。不要一次性丢十个接口下去,微信小程序并发请求数有限制,请求多了反而慢。
所有静态资源必须走 CDN,并且图片要压缩。小程序的 image 组件支持通过 CDN 参数实时缩略,比如?imageMogr2/thumbnail/400x,列表用小图,详情页才用原图。千万不要在列表页直接渲染大图,那个流量费用和体验都很酸爽。
页面骨架屏要做。小程序页面文字加载完成后,内容突然从空白跳到完整状态,用户会觉得很突兀。用简单的骨架屏组件,能在接口返回前先画出一个灰色占位结构,体感会舒服很多。
6. Vue 管理后台不容忽视的细节
管理后台虽然是内部系统,但它是博物馆运营人员每天要面对的东西。界面丑一点没关系,但权限和内容编辑体验必须过关。
6.1 菜单权限怎么设计才不会越权
后台用户分角色,角色分权限。具体到页面级别的权限,我习惯做成“菜单权限 + 按钮权限”两层。
菜单权限是用户在登录后,由后端根据角色返回一个菜单树,前端把它转换成动态路由。前端在拿到菜单后,用import.meta.glob动态加载对应的 Vue 组件:
const modules = import.meta.glob('./views/**/*.vue');这里的核心坑是:后端只返回菜单的 path 和名称,前端必须能通过 path 找到组件。所以菜单表里要有一个component_path字段,存着组件在项目里的相对路径,前端拿到以后动态加载。
按钮权限用自定义指令控制,比如v-permission="'order:export'"。没有权限的时候指令把 DOM 移除或禁用。这个只是视觉上的控制,后端接口必须再次校验权限,不能只靠前端隐藏按钮,不然随便调一下接口就绕过权限了。
6.2 内容录入:富文本、图片上传与 CDN 回源
博物馆内容运营人员不是程序员,他们不会写 HTML,也不理解为什么一张图 10MB 传不上去。管理后台的富文本编辑器必须要做到“所见即所得”。
我的做法是对上传组件做统一处理。不管图片、音频还是视频,都先在前端做检查,再传到对象存储,最后把 URL 回填到富文本编辑器。这里的检查包括格式、大小、分辨率。
需要注意的一个点是:对象存储上传成功后,不能直接把外网 URL 存成最终地址。最好把文件的“存储路径”存到内容表里,展示的时候再拼出 CDN 地址。这样以后更换域名或换云厂商,只需要改一层拼接逻辑,不用重刷整个内容表。
还要做版本管理。运营人员改一个展品描述,改完发现新版本不如旧版本,应该能一键回滚。这个实现不复杂,就是内容表加一个版本号字段,修改时保留旧版本,回滚时把历史版本复制回当前版本。
7. 联调、部署与上线排障
项目真正让人头疼的不是开发,是联调阶段和上线以后的第一次高峰。
7.1 环境配置与真机调试
多环境配置是必须一开始就做好的。开发环境、测试环境、生产环境至少要三套配置。
开发时,后端接口启动在局域网 IP,Vite 配置 proxy 把请求转发过去,小程序开发者工具直接填局域网地址。这里有一个常识坑:小程序开发者工具可以在“本地设置”里勾选不校验合法域名,但真机预览时这个设置不生效,真机必须使用 HTTPS。
真机调试的步骤要制度化:先联调导航、再联调支付、再联调地理位置。支付回调真机测不了,只能后端用测试工具模拟微信服务器请求,或者配置一个测试回调地址。
7.2 上线前必须检查的小程序设置
小程序审核被拒,大部分不是因为代码,而是因为资质和配置。
第一,小程序类目要选对。做博物馆票务和文旅服务,类目一般落在“旅游 - 景区服务”或“生活服务 - 旅游”,不同类目需要的资质文件不同。这个必须提前问清楚,不能等提审被拒了再补。
第二,隐私协议。小程序要收集位置、用户手机号、微信昵称头像、相册权限,都必须提前在小程序后台配置“用户隐私保护指引”,并且前端要弹出隐私协议弹窗。最近几年审核对这个卡得很死,漏一项就驳回。
第三,内容合规。博物馆的文物图片、讲解文字可能涉及版权和不允许公开的信息。博物馆侧要有一个审核流程,不是所有运营人员都能直接发布。
第四,域名白名单。小程序的 request 合法域名只认 HTTPS,并且要加到小程序后台,线上环境不要用 IP 访问,备案域名是底线。
7.3 常见问题与排查实录
以下问题是实际项目中反复出现,也是我认为做这类系统最应该提前预防的。
| 问题现象 | 根本原因 | 解决思路 |
|---|---|---|
| 门票超卖 | 库存扣减不是原子操作 | 用带条件的 update 或 Redis Lua 脚本,不能先查后改 |
| 重复下单 | 前端快速双击,接口被调用两次 | 后端加幂等注解,前端提交按钮加 loading 锁 |
| 支付回调漏单 | 回调处理失败未重试 | 回调记录表 + 唯一索引,失败要记录并人工补偿 |
| 小程序登录失效 | token 过期后没有刷新机制 | refresh_token 自动刷新,401 拦截后重放请求 |
| 图片加载慢 | 原图直出,没有压缩缩放 | 走 CDN 处理参数,列表用小图,详情用大图 |
| 蓝牙定位漂移 | 信号抖动,游客走动 | 滑动窗口去重 + RSSI 过滤 + 按展区聚合 |
| 后台菜单空白 | 后端返回菜单但前端找不到组件 | 组件路径字段要规范,动态路由加载要兼容路径大小写 |
| 内容更新不生效 | 客户端缓存了旧的 CDN URL | URL 加版本号参数,发布时强制刷新缓存 |
8. 做了这类项目后,我的几点实战心得
最后一个部分,不写总结,只分享几个实实在在的观点。
第一,不要一开始就上微服务。博物馆小程序项目的特点是流量有波峰波谷,但还没到需要分布式事务到处飞的规模。单体应用加 Redis、加消息队列,足以扛住热门场馆的抢票量。微服务是演进出来的,不是设计出来的,先跑起来比架构完美更重要。
第二,统一接口返回体这件事越早做越好。{code, message, data}这个结构看似简单,但如果你不加约束,前端团队会自己各写一套处理逻辑。错误码要由后端统一定义,前端只认 code,不识别后端返回的字符串。不然排查问题的时候,你会看到前端代码里全是针对不同接口写的特判逻辑。
第三,日志必须带 traceId。小程序端发起一个请求,链路可能是小程序 -> Nginx -> Spring Boot -> Redis -> MySQL。如果没有 traceId,用户反馈“我下单失败了”,你连这个请求到底到没到后端都查不出来。在网关或过滤器生成一个 traceId,塞到 MDC 里,打印到日志中,前端 return 的时候也把它带回。排查问题的速度提升一倍。
第四,给运营人员留好后路。博物馆不是所有内容都能预先设计好的,今天可能加一个临时特展,明天可能要换一个讲解音频。内容模型和后台操作流程一定要设计得足够灵活,最好能做到“不发布新版本就能上线新活动”。
最后再分享一个小技巧:小程序端启动时把“版本号、渠道来源、登录时间”上报到底层日志,后端拿到这些数据可以分析出老用户和新用户的活跃差异。我试过很多种方案,这个方法在监测内容更新和新版本回退时最直观,比各种埋点工具都好排查。