ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

uni-app + Spring Boot移动全栈框架:一套代码开通App、小程序、H5与管理后台

uni-app + Spring Boot移动全栈框架:一套代码开通App、小程序、H5与管理后台 做了几年移动端全栈开发接触过不少号称“一套代码搞定App小程序H5”的框架大部分宣传都很漂亮真拿到手能直接跑通、写清前后端交互、还带完整业务闭环的却不多。最近在整理公司内部技术栈的时候把这套沉淀了近两年的 App 移动开发框架重新梳理了一遍趁着热乎劲儿分享出来。它不是那种只有几个 demo 页面拼凑的教学代码而是把 App 端uni-app、后端服务、管理后台、小程序源码全部打通的完整工程拿到就能跑、业务能扩展。如果你正在做商城类小程序、行业类交易App或者想系统化地理解前后端分离项目怎么搭这套框架能帮你省掉大量从零铺路的时间。这篇分享我会把框架的选型思路、源码结构、前后端打通方式、核心业务实现、本地部署和二次开发路径全部拆开讲包括实际开发中踩过的一些坑。内容比较长建议先收藏再慢慢看。1. 这套框架到底解决什么问题1.1 一套代码要跑通三端还要扛住业务迭代先聊一下当初做这套框架的出发点。市面上做跨端移动开发的无非就这么几条路纯原生iOS/Android各写一套用 React Native 或 Flutter 做跨平台再就是基于 Web 技术的 hybrid 方案。纯原生开发对中小团队来说成本和周期都太奢侈React Native 和 Flutter 虽然跨端能力强但遇到小程序场景并不算友好该单独写的还是得单独写。现在做移动端项目几乎绕不开“App 小程序 公众号/H5”这个组合。小程序因为微信生态的流量优势基本是每个项目的标配需求App 则要覆盖 iOS 和 Android 两张系统管理后台往往又要另起一套 Web 工程。如果每一端都从零开发光是登录、支付、权限这老三样就够团队忙活一两个月。所以我选择了以 uni-app 作为客户端统一框架一套代码同时编译到 iOS App、Android App、微信小程序和 H5。服务端采用成熟的 Java 技术栈以 Spring Boot 为基础做了一整套 RBAC 权限、用户体系、订单支付等公共模块前端管理后台用 Vue 系列技术栈实现。这样整套框架下来一个人或者一个三人小团队就能撑起一个商业项目从立项到上线的全部工作量这是纯粹的分端开发很难做到的。1.2 技术选型不是选最火的而是选最不容易出错的这里多说几句技术选型的考量。选 uni-app 而不是 Flutter最核心的原因是小程序生态。uni-app 编译到微信小程序端是在小程序的运行环境下执行对小程序原生能力的调用支持已经很成熟了什么登录、支付、分享、定位、地图官方插件和社区方案都齐全。Flutter 在小程序这端的方案一直是绕路走做浏览器渲染的小程序还好要做微信原生小程序基本是无解的。后端用 Spring Boot 而不是 Node.js 或者 Go倒不是说谁比谁强而是在电商、交易、后台管理这类场景下Java 生态里的现成解决方案太丰富了。权限框架、工作流引擎、支付对账、消息队列这些都是经过大量生产环境验证过的出了问题能找到的参考案例也多。对应到项目人才市场上招聘成本也更友好。管理后台选型 Vue 技术栈是因为它和 uni-app 在语法习惯上比较接近工程内可以做到前端人员无感切换。真要细究Vue 这个方向的趋势还是稳的而且组件库选择空间大后文会讲到具体使用了哪些组件。2. 源码目录与工程拆解2.1 客户端源码pages、components、store、api 的分层逻辑拿到源码后建议先看根目录工程主要分成 client客户端、server后端、admin管理后台三个代码仓或三个大目录。客户端目录里比较重要的几个子目录client/ ├── pages/ │ ├── index/ # 首页 │ ├── goods/ # 商品列表/详情 │ ├── cart/ # 购物车 │ ├── order/ # 订单确认/列表/详情 │ ├── user/ # 个人中心/登录注册 │ └── webview/ # WebView内嵌H5页面 ├── components/ │ ├── goods-card/ # 商品卡片组件 │ ├── price-text/ # 价格展示组件千分位处理 │ ├── count-down/ # 秒杀倒计时组件 │ ├── load-more/ # 上拉加载更多 │ └── empty-view/ # 空态占位组件 ├── store/ │ ├── index.js # Vuex入口 │ ├── user.js # 用户状态模块 │ └── cart.js # 购物车状态模块 ├── api/ │ ├── request.js # 统一请求封装 │ ├── auth.js # 登录鉴权接口 │ ├── goods.js # 商品接口 │ ├── order.js # 订单接口 │ └── pay.js # 支付接口 ├── static/ ├── utils/ ├── App.vue ├── main.js ├── manifest.json ├── pages.json └── uni.scss页面、组件、状态、接口四层分离这个分法看起来简单实际上是在项目迭代过程中踩过不少坑之后才固化下来的。最开始的时候页面里直接写请求接口地址满天飞后来把所有接口统一收敛到api目录一个页面只对应一个接口模块文件后端改地址、加参数时只动一个文件就可以了。store里通常只放用户信息、购物车数量这类全局状态页面的临时数据不要往里面塞不然后期状态流乱到你不敢重构。pages.json是 uni-app 的路由和页面配置文件tabBar 在哪个页面、窗口导航栏样式、页面之间传参限制都在这里统一定义。实际项目里pages.json写得多细直接决定后期做小程序时是否会被微信平台的条条框框卡脖子后面讲小程序适配时我再展开。2.2 后端目录Controller、Service、Mapper 三层怎么组织后端部分是基于经典的 Spring Boot 工程结构再按业务域拆包的server/ ├── src/main/java/com/xxx/ │ ├── common/ # 通用返回体、异常、常量 │ ├── config/ # 拦截器/过滤器/跨域/MyBatis配置 │ ├── module/ │ │ ├── user/ # 用户模块 │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ ├── mapper/ │ │ │ └── entity/ │ │ ├── goods/ # 商品模块 │ │ ├── order/ # 订单模块 │ │ ├── pay/ # 支付模块 │ │ └── system/ # 系统管理菜单/角色/用户这种按业务域分包的方式比按 controller、service、mapper 分层分包更容易维护。建议新项目直接抄这个结构尤其是团队不大、一人要顶好几个模块的时候新同事接手后只需要看一个业务域就能快速进入状态不需要在整个工程里翻上下文。Controller 层只做参数接收和结果分发Service 层承载业务逻辑Mapper 层只做数据访问严格层级禁止 Service 跨层调用其他模块的 Mapper这是这套框架保持长时间可维护的底线。值得一提的是代码里大量用到了 MyBatis-Plus 的 Lambda 查询方式相比手写 XML 减少了大量重复 CRUD 代码。只有在复杂报表、多表联查时才引入自定义 SQL这样能兼顾开发效率和查询性能。2.3 管理后台和公共模块不止是添加商品、审核订单管理后台是一个标准的 Vue 工程包含商品管理、订单管理、用户管理、菜单权限管理、系统配置、支付流水等功能。这块很多人容易低估其实一个项目能不能顺利运营管理后台的设计水平很关键。框架里的管理后台做了几个值得复用的能力动态路由也就是根据登录用户的角色权限动态生成菜单而不是写死路由表数据字典能力商品状态、订单状态这些枚举值用数据字典维护前端下拉直接读接口改状态名称不需要改代码发版文件上传统一走中心化上传接口可以直接对接本地存储或者云对象存储业务方自行决定。3. 前后端交互是怎么一层层打通的3.1 统一响应结构code/message/data 是约定不是建议前后端交互最怕的就是各写各的后端返回成功一个格式、失败又一个格式前端拿到结果还要猜。这套框架里后端所有接口统一返回ResultT结构{ code: 200, // 200成功401未登录403无权限500业务异常 message: success, data: { ... } }别觉得这就是个简单的包装类真正的价值在于前端可以做统一拦截。客户端api/request.js里封装了完整的请求流程自动附加 token、统一展示错误提示、401 自动跳转登录页。这样每个页面不用写繁琐的错误处理逻辑写接口时只需要关心业务数据本身可以显著减少重复代码量。// api/request.js 核心逻辑 const request (options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || }, success: (res) { if (res.statusCode 401) { uni.removeStorageSync(token); uni.navigateTo({ url: /pages/login/login }); reject(res); return; } if (res.data.code 200) { resolve(res.data.data); } else { uni.showToast({ title: res.data.message, icon: none }); reject(res.data); } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); };这个封装里有个容易被忽视的点401和res.data.code 200是两层判断。第一层是 HTTP 状态码第二层是业务状态码。后端对于一些参数校验错误、业务规则不满足的情况应该返回 HTTP 200 但业务 code 非 200让前端正常拿到message展示。只有未登录、token 过期这类真正需要前端做跳转的情况才返回 HTTP 401。3.2 Token 鉴权从登录到过期刷新的完整闭环用户登录后后端会签发一个 token客户端把它存到本地存储。后续所有请求在拦截器里自动把 token 塞进Authorization请求头后端通过拦截器统一校验。这套框架采用的方案是token 里保存用户 ID 和过期时间后端通过 Redis 维护 token 与用户会话的映射关系。用户退出登录或修改密码时直接删除 Redis 里的 token实现服务端主动失效。关于 token 过期时间有个经验值可以供参考App 端可以放宽到 7 天小程序端因为微信会话存在过期风险通常设置到 2 天并且前端要做 401 后的静默刷新逻辑。如果前端不做静默刷新用户玩着玩着突然跳登录页体验会非常割裂。静默刷新的实现思路是当请求返回 401 时先用 refreshToken 去换新的 accessToken换完再重放原来的请求全部在拦截器里透明完成。// 静默刷新逻辑伪代码 if (res.statusCode 401 !options.isRetry) { const refreshToken uni.getStorageSync(refreshToken); if (refreshToken) { const newToken await refreshAccessToken(refreshToken); uni.setStorageSync(token, newToken); options.isRetry true; return request(options); // 重放原请求 } }3.3 权限模型什么角色看什么菜单什么接口能调到后端用经典的 RBAC 模型用户关联角色角色关联菜单按钮权限。登录的时候把当前用户的权限标识列表比如goods:add、order:export返回给前端前端根据权限标识控制页面上的按钮显隐。别小看按钮级权限很多项目做到菜单级权限就停了结果用户进入页面后能看到一堆按钮点一下提示无权限体验很差。而真正在按钮层做控制之后管理后台的体验会上升一个台阶。开发时只要在接口服务上加上PreAuthorize(hasAuthority(goods:add))注解前端拿到权限列表后动态判断按钮v-if即可。3.4 跨域、代理和本地联调实际开发中最容易卡壳的环节前后端分离开发中跨域问题应该是最常见的了。管理员台上用的是 Vite 开发服务器配置代理把/api开头的请求转发到后端地址生产部署时再用 Nginx 做反向代理。客户端因为跑在手机或微信开发者工具里没有浏览器的同源策略那么严格但也要注意请求的域名必须是 HTTPS 且在小程序后台配置为合法请求域名。实际联调时建议一步到位后端启动后确认监听 0.0.0.0 而不是 127.0.0.1保证同一局域网内的真机可以访问小程序开发者工具里勾选“不校验合法域名”开发期就可以直接用 IP 地址访问本地后端。这个配置只用于开发阶段上线前务必关闭并切换到正式域名否则审核会被卡。4. 核心业务模块的落地细节4.1 商城模块商品、购物车、订单、支付的完整闭环商城是这套框架里业务最完整的一个模块完整跑通了“商品浏览-加入购物车-提交订单-在线支付-订单状态流转”这条链路。商品模型支持多规格颜色、尺码、多图册、上下架状态、库存扣减购物车数据同时支持本地未登录状态下的临时存储和登录后的云端同步避免用户辛辛苦苦选了一堆商品一登录反而清空了。订单状态的设计遵循常规电商标准待支付、待发货、待收货、已完成、已取消。支付模块目前实现了微信支付小程序/App和模拟支付两种模式。模拟支付这个能力非常实用开发阶段不用真实申请微信支付商户号就能打通整条链路联调时切换到真实支付即可线上启用。这块我最想强调的是库存扣减的逻辑。代码里没有用简单的先查询再更新这种很容易超卖的方式而是用一条条件更新 SQLUPDATE goods_sku SET stock stock - #{num} WHERE id #{skuId} AND stock #{num}然后通过受影响行数判断是否扣减成功失败则提示库存不足。这虽然比不上分布式锁方案那么“重量级”但在单库单表的中小规模场景下性能和正确性都够了代码也好理解。4.2 小程序端几个容易被忽略的细节同样是这套代码在小程序端的适配上有几个细节非常值得关注第一个是动态设置页面标题。uni-app 里可以通过uni.setNavigationBarTitle在运行时修改当前页面导航栏标题比如商品详情页标题改成商品名称、订单详情页标题改成订单号。但要注意这个 API 在小程序端的生效时机有限制最好在onLoad或首次渲染前调用。如果想在页面之间跳转时带标题过去建议使用eventChannel或者直接在 URL 参数里带上 title在目标页面的onLoad(options)里读取后调用。第二个是 tabBar 和自定义导航栏的高度问题。小程序不同机型的顶部状态栏高度不同底部 tabBar 高度也不同。框架里封装了获取状态栏、胶囊按钮位置的工具函数自定义导航栏时可以精确计算内容区域的高度避免页面内容被刘海屏遮住。第三个是页面栈的限制。小程序页面栈最多只有十层连续跳转太多层级会静默失败。框架里写了统一的路由跳转工具方法判断页面栈深度后自动选择navigateTo还是redirectTo避免线上出现“按钮点了没反应”这种不好排查的问题。4.3 App 端打包与真机调试云打包虽然省事但坑也不少App 端建议用 HBuilderX 自带的云打包服务免去本地安装 Android SDK 和 Xcode 的麻烦。但有几个点要提前注意manifest.json里必须配置正确的 App 图标和应用名称登录、支付、分享等模块用到了原生插件打包时需要勾选对应的模块权限Android 平台需要配置包名和证书指纹否则微信支付、分享功能会调不起来。真机调试时iOS 和 Android 有点小差异。Android 可以直接开启 USB 调试用 HBuilderX 真机运行插件生效最快iOS 建议用标准基座配合自定义调试基座Loader 模式下可以实时同步修改适合 UI 快速调整阶段。5. 本地搭建与线上部署5.1 五分钟把框架跑起来从拿到源码到本地能跑通正常十分钟内可以完成。建议按下面的顺序操作可以少走弯路导入数据库脚本。后端根目录下有个sql文件夹用 Navicat 或命令行执行初始化脚本里面包含表结构和基础数据。修改后端配置。application.yml里需要改三处数据库连接信息、Redis 连接信息、文件上传存储路径。启动后端。用 IDEA 导入后端工程等 Maven 依赖下载完成后启动Application.java。看到 Spring Boot 启动成功的日志就是起来了。启动管理后台。进入admin目录执行npm install然后npm run dev默认端口通常是 8080 或 5173。启动客户端。用 HBuilderX 导入client目录修改utils/config.js里的BASE_URL先跑 H5 端能通说明前后端通了一遍。跑通之后先到管理后台创建一个测试商品再到客户端首页刷新能看到商品列表说明全链路已经正常。5.2 配置项里需要预先想清楚的内容数据库脚本里的初始账号管理员 admin 和普通用户两个账号的密码是 MD5 加盐加密存储的。这里提醒一下正式上线一定要改掉默认密码并且在代码里把登录模块的密码加密逻辑从 MD5 升级为 BCrypt。另外Redis 的密码也建议在部署前设置好默认不带密码的配置只适合本地开发环境。5.3 服务器部署的推荐姿势部署线上环境我推荐的结构是一台 4C8G 的云服务器上面跑 Nginx、后端 Jar 包、Redis、MySQL以中小型项目初期规模来说完全够用。这种单机部署虽然看起来没那么“高可用”但胜在架构简单、成本可控出问题好排查。后端用mvn package打成 Jar 包然后用nohup java -jar xxx.jar 启动。如果要开机自启或异常重启可以用 systemd 管理。Nginx 需要配置三个 location前端静态资源、后端接口反向代理、上传文件的静态访问路径。server { listen 443 ssl; server_name yourdomain.com; # 前端页面 location / { root /var/www/html; index index.html; try_files $uri $uri/ /index.html; } # 后端接口 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 上传文件访问 location /uploads/ { alias /data/uploads/; } }注意管理后台是独立的前端工程线上也需要单独部署可以是同域名不同路径也可以是不同子域名。为了省事可以把管理后台构建出的静态文件放到另一个目录用location /admin/去代理。6. 二次开发从“能跑”到“改成自己的项目”6.1 换掉默认信息新增一个全新项目的最小改动清单把框架用作自己项目时有一份最小改动清单按顺序走一遍项目基本就是你的了修改客户端manifest.jsonApp 名称、小程序 AppID、logo、版本号。修改后端包名和全局配置将com.xxx改成自己公司的域名反写同时同步改掉application.yml里的项目名。修改管理后台的标题和 logo一般都在公共布局组件里。替换数据库前缀如果表名带了xxx_前缀批量替换成自己的前缀。删除框架自带的示例页面和示例数据保留业务骨架。6.2 新增一个业务模块的完整路径新增模块不要从零写。建议复制一个现有的简单模块比如“公告通知”模块然后逐步替换成自己的业务字段。这样比从 Controller 一路手写到 Mapper 快得多也可以避免因为某个配置文件忘记处理而踩坑。后端要改的文件有实体类、Mapper、Service、Controller、权限标识前端管理后台要新增页面或路由菜单客户端看场景决定是否新增页面。整套操作走完一个带增删改查、权限控制、前端页面的业务模块快的半小时内能做出来。6.3 小程序发布前的备案与合规检查现在微信小程序上线前要求办备案这件事要提前规划不然后面上线会很被动。备案流程通常需要提供主体信息、管理员信息、小程序名称平台审核一般几天到两周不等建议小程序开发完成度到 80% 时就提交备案和后续联调、测试并行进行能省出不少时间。发布前还要在小程序后台配置服务器域名和业务域名。涉及支付的小程序支付目录也要在商户平台配置正确。如果觉得域名配置麻烦开发阶段可以用调试模式跳过但正式版本是绕不开这一步的。7. 常见问题与排查实录7.1 接口请求失败、提示“网络异常”或“无法连接服务器”这类问题优先从三方面排查确认后端的 IP 和端口是否可以从当前设备访问手机和小程序开发者工具往往不在同一网络最容易出问题的是连了公司 WiFi 却访问本地笔记本电脑上的后端需要在防火墙里放行端口确认BASE_URL填的是不是带http://或https://前缀的完整地址确认小程序的开发环境是否勾选了“不校验合法域名”。7.2 用户明明登录了但接口一直返回 401大多数情况是 token 没传过去或者 token 过期了。打开浏览器调试工具看请求头的Authorization字段是否带了值如果带了再看后端日志里 token 解析环节是否报错。还有一个容易忽视角是多个应用共用一套后端时token 可能被另一个应用的登录挤掉了此时需要检查 Redis 里的 key 设计是否包含了应用维度。7.3 小程序端图片加载不出来小程序端页面图片加载不出来的原因往往不是前端代码问题而是图片域名没有配置到小程序的“downloadFile 合法域名”里。临时解决方式是用开发者工具的“不校验合法域名”正式上线必须在后台配置好域名而且域名必须是 HTTPS不能用 IP。7.4 真机调试时App 端一切正常小程序端白屏白屏问题十有八九是 JS 运行错误。先看 console 面板有没有报错再看页面路径是不是写对了。小程序端对 ES6 语法的兼容性比 H5 端严格代码里一些较新的 API 需要转译。框架里已经配置好了babel转译但如果自己新增了第三方依赖尤其是直接从原生小程序迁移过来的组件要留意有没有用到小程序端不支持的能力。7.5 表格形式总结问题速查异常现象最可能的原因排查/解决方向请求全部失败BASE_URL 配置错误检查客户端/前端的全局 API 地址401 高频出现token 过期未静默刷新检查 refreshToken 时序与 Redis 过期时间图片不显示合法域名未配置小程序后台添加 downloadFile 域名白屏JS 报错或页面路径错误看 console、检查 pages.json支付调不起来支付参数未配置检查商户号、证书、Android 包名签名上传文件失败存储路径无权限检查目录写权限和后端配置7.6 一些实际踩坑后的经验总结这套框架在业务扩展方面很灵活商城模块里的订单体系可以直接复用到苗木交易、礼品登记这类场景里商品与订单骨架是可插拔的把中间的字段替换成自己的业务字段就能跑新项目。如果要在小程序端做一些更复杂的交互比如语音指令识别框架里预留了事件总线机制前端采集音频、后端接语音识别引擎、再映射回业务事件整个过程对主业务是无侵入的二次开发时不需要动主体架构。我个人的习惯是在任何一个模块改代码之前先把原有的业务流程在纸上画一遍尤其是订单、库存、状态机这种有隐藏规则的模块不要上来就改。框架给的权限控制和前后端交互那套信令体系尽量保留这是整个工程最值钱的部分。换一套交互方式容易但后期维护成本会成倍增加。大家拿到框架后如果碰到资源加载、端口占用、证书配置这类问题多半是本地环境差异引起的先对照官方文档检查一遍大部分都能解决。真正有价值的经验都是在跑通第一遍之后开始改业务逻辑时获得的找一个具体的业务场景把它从表结构到页面完整走一遍这套框架的含金量才能真正体现出来。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进