尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Spring Boot+Vue3+Uniapp点餐小程序全栈开发实战与上架指南

Spring Boot+Vue3+Uniapp点餐小程序全栈开发实战与上架指南 简介本资源是一套基于Spring Boot Vue3 UniApp技术栈开发的完整点餐小程序源码面向Java后端、Vue前端及跨端小程序开发者解决多端统一交付与高复用业务系统快速搭建问题。压缩包共499个文件含78个Java类如SysGoodsController、UserOrderServiceImpl等核心业务逻辑、127个Vue组件实现商品展示、订单管理、用户交互等界面、58个JS/40个TS脚本支撑UniApp多端适配与状态管理、40个PNG图标资源及配置类YML、JSON、XML文件整体大小19.16MB。已有832人学习下载。资源结构遵循标准工程规范后端采用Spring Boot RESTful API提供稳定服务前端通过Vue3 Composition API组织模块化组件UniApp层封装平台差异并支持一键编译至微信小程序、H5及App附带完整数据库交互、用户认证、订单流程与商品评论等真实业务闭环代码可直接用于教学实践、毕设开发或中小餐饮项目快速落地。 做点餐小程序这个项目我前后花了大概三周时间把一整套流程完整跑通从后端接口设计到前端页面渲染再到兼容安卓和iOS的H5端、小程序端最终成功上线。过程中踩了不少坑尤其是Spring Boot版本选择、Vue3组合式API在小程序端的表现以及uniapp上架安卓应用市场时的各种配置问题。这篇就把整个项目的完整落地过程写出来从技术选型、表结构设计、核心接口实现到前端状态管理、跨端适配、上架审核每一步都带有实际的代码和踩坑记录希望对正在做同类项目或准备做毕设的同学有参考价值。1. 为什么是Spring BootVue3Uniapp这一套组合先聊选型。这个点餐小程序的定位是面向中小餐饮商家的轻量级解决方案需要覆盖用户点餐、商家管理、后台运营三个端口分别对应小程序端、商家H5管理端、PC管理后台。选型时考虑过几套方案最终确定Spring BootVue3Uniapp原因很直接。后端用Spring Boot理由简单粗暴——生态成熟、上手快、招人容易而且对于点餐这种以CRUD为主、带一点订单状态流转的业务场景Spring Boot的约定优于配置能让开发效率翻倍。版本这里有个重要提醒千万别无脑上最新版。我之前用过Spring Boot 3.x结果遇到很多第三方库还没适配Jakarta命名空间的问题比如一些老的MyBatis插件、代码生成器直接跑不起来光是迁移javax到jakarta就折腾了半天。这个项目里用的是Spring Boot 2.7.x稳定、兼容性强大部分轮子都是现成的。前端管理端选Vue3用户端用uniapp。Vue3的组合式APIComposition API在管理端这种多模块复杂页面里优势非常明显特别是做点餐菜品管理、订单列表这种需要大量状态共享的场景。而uniapp能让一套代码同时编译到微信小程序、支付宝小程序、H5和App对中小商家来说多端覆盖意味着多一个流量入口这个诱惑力很大。选uniapp还有一个关键理由它的条件编译机制可以很好地处理各平台的差异。比如微信小程序里要使用微信支付支付宝小程序里要使用支付宝支付这些通过#ifdef条件编译就能优雅地实现平台差异化逻辑而不需要维护多套代码。不过也要说句公道话uniapp在复杂交互场景下偶尔会有一些性能问题比如长列表渲染、复杂动画等但这个项目里点餐流程的交互并不算复杂商品列表用mescroll-uni做分页加载性能完全够用。如果做的是那种带即时聊天、实时音视频的大型应用可能还是要考虑原生开发。整套技术栈下来个人的体会是Spring Boot负责业务逻辑和数据持久化Vue3负责管理端的复杂交互uniapp负责用户端的跨端覆盖各司其职配合默契。2. 后端核心模块与表结构设计先从下单流程倒推数据模型后端设计我是从“用户完整下单流程”倒推的这样表结构更贴近实际业务。整个点餐流程是这样的用户进入小程序选择门店然后浏览菜品分类、加购物车提交订单商家接单后开始制作用户到店自取或外卖配送最后完成订单。围绕这条链路核心表有8张门店表、菜品分类表、菜品表、购物车表、订单表、订单明细表、用户表、商家表。门店表字段不多但有个细节值得注意经纬度字段建议用DECIMAL(10, 6)类型存储而不是直接用浮点型避免精度丢失。别小看这个细节后续如果做“附近门店”功能经纬度有精度问题会导致距离计算偏差很大。菜品表是整个菜单模块的核心CREATE TABLE dish ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, store_id bigint NOT NULL COMMENT 所属门店ID, category_id bigint NOT NULL COMMENT 分类ID, name varchar(64) NOT NULL COMMENT 菜品名称, price decimal(10,2) NOT NULL COMMENT 售价, original_price decimal(10,2) DEFAULT NULL COMMENT 原价用于展示折扣, image varchar(255) DEFAULT NULL COMMENT 菜品图片, description varchar(500) DEFAULT NULL COMMENT 描述, stock int DEFAULT 0 COMMENT 库存-1表示不限量, status tinyint DEFAULT 1 COMMENT 状态1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_store_category (store_id, category_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品表;联合索引idx_store_category很关键。点餐场景下用户每次打开菜单小程序都会请求“某个门店下的某个分类的菜品列表”如果没有这个联合索引数据量上来之后这个查询会非常慢。我在这个项目里加了这个索引后一万多条菜品数据查询耗时从原来的300多毫秒降到了20毫秒左右效果立竿见影。订单表是另一个需要重点设计的表。外卖和到店自取要区分订单状态要清晰支付信息要完整。我的订单表设计包括订单号、用户ID、门店ID、订单类型1自取 2外卖、订单状态、支付状态、支付方式、订单金额、实付金额、收货地址、备注、下单时间、支付时间、完成时间等字段。订单号生成有个坑要特别注意不要用数据库自增ID直接当订单号很容易被竞争对手推算出单量。我用了“门店ID后四位时间戳随机四位”的方式生成业务订单号比如1001202312111435001234。这样既保证唯一性又不会泄露真实单量。订单状态流转是点餐系统的核心我用了状态机模式状态含义可流转到的状态0待支付1、61待接单2、72制作中33待取餐/待配送44完成-5已评价-6已取消-7已退款-这个状态机的设计原则是“每个状态只允许它该有的流转”比如待支付订单只能取消或支付成功变待接单不能直接跳成已完成。我在后端有一个OrderStateMachine类专门管理状态的合法性校验避免出现“订单都没支付就直接完成”的脏数据问题。订单明细表也有讲究不仅要记录菜品ID、名称、价格、数量还要把当时下单的菜品快照存进去包括菜品的图片、规格、口味。为什么要存快照因为菜品信息是可变的今天卖18块、明天搞活动卖15块但已经下单的订单在用户端应该显示的是下单时的价格不是现在改过的价格。很多新手容易忽略这一点导致用户看到的历史订单金额和当前菜品价格对不上。这里建议用MyBatis-Plus作为持久层框架真的能提升效率。内置的LambdaQueryWrapper写条件查询非常方便比如查某门店的菜品列表PageDish page dishMapper.selectPage(new Page(pageNum, pageSize), new LambdaQueryWrapperDish() .eq(Dish::getStoreId, storeId) .eq(Dish::getStatus, 1) .eq(Dish::getCategoryId, categoryId) .orderByDesc(Dish::getSort));代码简洁、类型安全而且不用手写XML映射。不过要注意复杂SQL还是建议用Select注解或XML灵活度更高比如统计销量排行这类要多表联查的报表SQL。3. 商品分类与菜单列表接口一次请求还是多次请求用户进店后第一个看到的就是菜单。这里有个设计决策菜单页的“左侧分类、右侧菜品”是请求一次接口返回全量数据还是左侧分类请求一次、右侧菜品按分类再请求一次我做的时候选择了后者也就是分类列表和菜品列表分开两个接口。原因有二一是小程序端首页加载速度更快首屏先拿到分类列表渲染左侧导航用户点击某个分类时再拉取对应菜品首屏数据量缩小了80%以上二是用户体验更流畅用户可以快速浏览各个分类而不需要等所有菜品一次性加载完。分类接口很简单GetMapping(/category/list) public ResultListCategoryVO list(RequestParam Long storeId) { ListCategoryVO list categoryService.listByStoreId(storeId); return Result.success(list); }菜品列表接口带分页和缓存GetMapping(/dish/list) public ResultPageResultDishVO list(RequestParam Long storeId, RequestParam Long categoryId, RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { // 优先查缓存未命中再查数据库 String cacheKey dish:list: storeId : categoryId : pageNum; PageResultDishVO result redisService.get(cacheKey); if (result null) { result dishService.pageByCategory(storeId, categoryId, pageNum, pageSize); redisService.set(cacheKey, result, Duration.ofMinutes(5)); } return Result.success(result); }菜品数据属于低频变更数据加了Redis缓存后菜单加载速度从平均150ms降到了30ms左右。这里要注意缓存刷新时机菜品上下架、价格调整时必须主动删除对应分类的缓存否则用户端看到的还是旧数据。我在菜品的增删改方法里用了CacheEvict注解每次操作完自动清除对应门店、对应分类下的菜品缓存。另一个容易被忽略的细节菜品列表的排序规则。我建议按“自定义排序权重销量”降序排列权重值越小越靠前这样商家可以把招牌菜、推荐菜排在最前面。代码里用的orderByDesc(Dish::getSort)再orderByDesc(Dish::getSales)用户看到的第一屏是商家想推荐的东西转化率会高不少。4. 购物车设计与实现前端缓存还是后端存储购物车是点餐流程里用户交互最多的模块。这里先抛一个结论购物车数据放在前端本地缓存uniapp的uni.setStorageSync就足够了不需要后端建表维护。为什么因为购物车本质上是一个临时容器用户加了菜没下单这个数据对商家毫无意义用户下单后购物车数据会以订单明细的形式落到后端又有据可查。很多初学者喜欢把购物车也做成后端接口但这样不仅增加了开发量还会带来一个问题用户频繁加菜、减菜每次都要请求后端服务器压力大用户体验还不好。本地缓存的方案可以做到秒开用户操作起来非常跟手。前端我用Vue3的reactive配合computed管理购物车状态。购物车的核心数据结构是Mapkey是菜品IDvalue是菜品信息和数量因为Map的增删改查都是O(1)复杂度特别适合这种频繁操作的场景。import { reactive, computed } from vue const cart reactive(new Map()) // 添加菜品 const addToCart (dish) { if (cart.has(dish.id)) { cart.get(dish.id).count } else { cart.set(dish.id, { ...dish, count: 1 }) } uni.setStorageSync(cart, JSON.stringify(Array.from(cart.entries()))) } // 计算总价 const totalPrice computed(() { let total 0 cart.forEach(item { total item.price * item.count }) return total.toFixed(2) }) // 计算总数量 const totalCount computed(() { let count 0 cart.forEach(item { count item.count }) return count })computed是Vue3里特别好用的API它会对依赖的数据进行缓存当cart变化时才重新计算总价和总数比Vue2的computed写法更直观。这里有个小细节每次addToCart后都要同步到uni.setStorageSync这样即使用户退出小程序再进来购物车数据也不会丢体验会好很多。有个购物车经典问题要提醒用户在小程序端加入购物车然后去PC端管理后台加菜或改价两端的数据是不同步的。但如果购物车设计成后端存储又会遇到“游客未登录也能加购”的矛盾。所以我才建议小程序端本地缓存购物车等真正提交订单时再让用户登录微信授权手机号或用openid直接创建账号这样既保证了体验又简化了架构。当然如果做的是那种需要多端实时同步购物车的SAAS产品那就要认真的设计后端购物车表了加一个member_id字段关联用户前端每次操作增删改后调后端接口同步。这个项目因为不需要多端同步所以不增加这个复杂度。5. 下单与库存扣减预扣还是支付后扣下单流程看似简单用户提交购物车数据后端生成订单调用微信支付同步通知商家。但里面藏着一个关键决策——库存扣减时机这个项目我踩过不少坑。先说说三种技术方案第一种是提交订单时直接扣库存支付失败再返还。这种方式最直观但问题在于如果一个用户提交了订单但迟迟不支付库存一直被占用其他用户就买不到了容易导致“僵尸订单饿死真实订单”的情况。第二种是支付成功后才扣库存。这种方案相对安全但存在超卖风险——两个用户同时提交了相同菜品的订单系统在支付回调里扣库存时发现库存不够了就会出现退单或体验不佳的问题。第三种是提交订单时先冻结库存即预扣支付超时未支付再释放库存。这是电商系统里比较标准的做法既能防止超卖又能通过超时释放机制避免库存长期被占用。我在这个项目里用的是“提交订单预扣库存 定时任务释放超时订单”的组合方案。用户在提交订单时后端会直接扣除菜品库存同时开启一个30分钟的倒计时如果30分钟内未完成支付定时任务会自动取消订单将已扣的库存回补库存。预扣库存的代码实现里有个关键技术点——数据库乐观锁防止并发超卖Update(UPDATE dish SET stock stock - #{count}, sales sales #{count} WHERE id #{dishId} AND stock #{count}) int deductStock(Param(dishId) Long dishId, Param(count) Integer count);SQL里加上了stock #{count}条件哪怕100个用户同时下单同一种菜品数据库层面也会保证只有库存足够的那几个请求能成功扣减库存其余请求要么重试要么友好提示“库存不足”。这个方法利用了数据库行锁的原子性比在Java代码里加Synchronized或者使用分布式锁简单得多也可靠得多。订单超时未支付自动取消我用的是Spring Boot的定时任务Scheduled(fixedDelay 60000) public void autoCancelExpiredOrders() { ListOrder expiredOrders orderMapper.selectExpiredOrders(30); for (Order order : expiredOrders) { orderService.cancelOrder(order.getOrderNo()); // 回补库存逻辑 ListOrderDetail details orderDetailMapper.selectByOrderNo(order.getOrderNo()); for (OrderDetail detail : details) { dishMapper.addStock(detail.getDishId(), detail.getQuantity()); } } }定时任务每隔一分钟扫描一次把超过30分钟未支付的订单自动取消并回补库存。这里有个小优化扫描时只查询状态为待支付且订单时间早于30分钟前的订单不要全表扫描否则数据量大了之后定时任务会越跑越慢。分布式环境下的定时任务有个要注意的点如果未来系统扩展成多实例部署这个Scheduled会在每个实例上都执行一次导致重复取消和重复回补库存。解决方式是用XXL-Job统一调度或者至少给取消订单的方法加一个幂等判断比如先查一次订单状态只有待支付状态的订单才执行取消我之前还用redis分布式锁控制过以免多个实例同时处理同一笔订单。6. 微信小程序端适配从顶部导航栏到自定义页面分享uniapp一套代码跑多端理想很丰满但真正落地时微信小程序端还是有一堆细节要折腾。这里记录几个我实际遇到并解决的高频问题。6.1 顶部导航栏高度的适配问题微信小程序和H5的顶部导航栏高度不一样安卓和iPhone也不一样这导致页面布局经常会出现“顶部被刘海遮挡”或者“按钮和状态栏重叠”的问题。解决方案是动态获取状态栏高度和导航栏高度然后给页面顶部留出安全的padding// 状态栏和导航栏高度计算 const getSystemInfo () { const systemInfo uni.getSystemInfoSync() const statusBarHeight systemInfo.statusBarHeight // 状态栏高度 const menuBtn uni.getMenuButtonBoundingClientRect() // 胶囊按钮位置 const navBarHeight (menuBtn.top - statusBarHeight) * 2 menuBtn.height return { statusBarHeight, navBarHeight } }这段代码的原理是微信小程序右上角胶囊按钮的高度和位置是固定的通过getMenuButtonBoundingClientRect()获取胶囊按钮的位置和高度就能反推出自定义导航栏应该在什么位置。这个方案在安卓和iOS上实测都稳定强烈建议在pages.json里统一配置navigationStyle: custom自定义导航栏视觉上可以和页面融为一体用户体验更好。6.2 自定义分享好友的坑点餐系统里“分享好友一起点餐”是个很常见的需求。用uniapp自带的onShareAppMessage钩子可以实现分享onShareAppMessage() { return { title: 【${this.storeName}】邀请你来点餐啦, path: /pages/index/index?storeId${this.storeId}inviter${this.inviterId}, imageUrl: this.shareImage } }这里有个很重要的点如果你想每次分享时都动态生成不同的分享文案或小程序码图片必须在onShareAppMessage里返回一个Promise而不是直接return对象因为微信小程序的分享回调有异步限制。如果直接掉接口获取自定义图片后再返回可能会拿不到想要的分享卡片效果。小程序码的生成也是一个重点。美团这种大厂的做法是货到付款我们当然做不到那种规模但可以用微信的getUnlimitedQRCode接口去生成小程序码然后通过canvas把店铺logo、菜品图片、价格等信息画上去最后uni.canvasToTempFilePath导出图片分享给好友。这里要注意canvas在小程序和H5端的API差异比较大建议把这块逻辑封装成独立工具类用条件编译分别处理。6.3 曼奈斯特配置manifest.json的常见问题uniapp的manifest.json是连接各端的桥梁很多初学同学在这儿踩过坑。微信小程序端配置注意几个字段{ mp-weixin: { appid: 你的小程序AppID, setting: { urlCheck: false, es6: true, postcss: true, minified: true }, usingComponents: true, permission: { scope.userLocation: { desc: 用于获取用户位置信息以推荐附近门店 } } } }urlCheck字段在开发阶段一定要设置为false否则每次请求后端接口都会提示“域名不合法”。但上线前记得改成true或者直接去微信公众平台配置合法域名。我见过不少同学开发时模拟器里接口通真机上一片空白就是这个问题。另外微信小程序对网络请求的合法域名有严格限制如果后端接口跑在http://localhost:8080或者http://192.168.1.100:8080真机调试是永远请求不通的。建议开发阶段用内网穿透工具把接口映射成HTTPS域名或者直接使用局域网IP配合“不校验合法域名”的调试模式。6.4 分包优化与首屏加载微信小程序有2MB的主包大小限制但对于带图片、带视频的点餐应用随便加几张商品图就超了。解决方案是使用分包加载把商品详情页、优惠券页面、订单列表页这类不是首屏必须的页面放进分包里主包只保留首页、分类页和购物车页。uniapp里的分包配置在pages.json里{ pages: [ pages/index/index, pages/category/category, pages/cart/cart ], subPackages: [ { root: pages/order, pages: [ order-confirm/order-confirm, order-list/order-list, order-detail/order-detail ] }, { root: pages/mine, pages: [ mine/mine, coupon/coupon, address/address ] } ], preloadRule: { pages/index/index: { network: all, packages: [pages/order] } } }这样配置后用户首次进入首页时不会加载订单分包的代码等到真正点单确认时才去加载首屏加载时间能减少40%左右。preloadRule的意思是用户进入首页后就预下载订单分包等到用户下单确认页分包代码已经提前缓存好了进入时几乎没有白屏时间。6.5 地图组件的选型问题点餐系统里经常需要展示门店位置热词里有人问“微信小程序可以使用天地图地图组件吗”。实测下来不太推荐因为微信小程序的地图组件map官方支持的主要是腾讯位置服务和微信自己的地图服务调用第三方地图服务天地图、高德、百度都需要通过web-view加载网页体验和原生地图组件差很多。建议直接用微信的原生map组件配合markers传门店坐标再通过wx.chooseLocation选择收货地址这是最省力也最稳定的方案。7. Vue3管理后台的实践defineProps和defineEmits的正确打开方式管理后台用的Vue3Element Plus这里聊聊Vue3组合式API在两个高频场景上的使用心得。网上关于defineProps和defineEmits的教程很多但很多都是简单示例真正用到业务里的细节坑没有说透。菜品编辑弹窗是一个很典型的“父组件传数据给子组件、子组件提交后通知父组件刷新”的场景父组件菜品列表页template el-dialog v-modeldialogVisible title编辑菜品 DishForm v-ifdialogVisible :dishcurrentDish submithandleSave canceldialogVisible false / /el-dialog /template script setup import { ref } from vue const dialogVisible ref(false) const currentDish ref(null) const handleSave (formData) { // 调用保存接口 saveDish(formData).then(() { dialogVisible.value false loadDishList() // 刷新列表 }) } /script子组件菜品表单script setup const props defineProps({ dish: { type: Object, default: () null } }) const emit defineEmits([submit, cancel]) // 表单数据 const form reactive({ name: , price: 0, categoryId: null, image: , description: , stock: 0 }) // 监听父组件传入的dish变化回填表单 watch(() props.dish, (val) { if (val) { Object.assign(form, val) } }, { immediate: true }) const handleSubmit () { emit(submit, { ...form }) } const handleCancel () { emit(cancel) } /script这里有三个容易踩的坑第一个是v-ifdialogVisible。如果不加这个条件子组件在父组件还没拿到数据时就会被渲染watch的immediate: true虽然能拿到null但后续如果父组件的currentDish一直是同一个对象引用watch可能不会触发更新导致表单里显示的还是上一个菜品的数据。用v-if保证每次打开弹窗都是全新渲染数据自然刷新。第二个是Object.assign(form, val)这个回填方式。如果form和val的字段名不一致比如后端返回的是category_id而下拉框绑定的是categoryId直接Object.assign会把错误的字段塞进表单。建议在回填时使用form.name val.name; form.categoryId val.categoryId这种手动赋值的方式虽然多写几行但不容易出脏数据。第三个是defineEmits的生命周期。子组件挂载前就会执行所以在onMounted里监听emit是不行的必须在setup顶层监听。上面的代码里直接在模板事件中调用emit(submit)就不存在这个问题。computed也是Vue3里被严重低估的一个API。在菜品管理后台我经常需要根据搜索条件动态过滤菜品列表const searchText ref() const categoryFilter ref(all) const statusFilter ref(all) const filteredDishList computed(() { return dishList.value.filter(dish { const matchName dish.name.includes(searchText.value) const matchCategory categoryFilter.value all || dish.categoryId categoryFilter.value const matchStatus statusFilter.value all || dish.status Number(statusFilter.value) return matchName matchCategory matchStatus }) })用computed的好处是它基于响应式依赖自动缓存结果当ssearchText、categoryFilter、statusFilter变化时才重新计算不会像watchmethods那样每次渲染都执行一遍过滤逻辑性能好不少。8. 上架与部署从开发环境到安卓应用商店的完整流程这个项目的部署上线过程让我对uniapp上架安卓应用市场有了完整的认知。这里面涉及的坑远超开发阶段遇到的所有问题总和。8.1 应用签名与证书的配置如果要把uniapp打包成APK上架安卓应用市场必须先生成签名证书。Android应用签名是应用的身份标识没有签名的APK无法安装到手机上。我用的命令是keytool -genkeypair -v -keystore myapp.keystore -alias myapp -keyalg RSA -keysize 2048 -validity 10000参数解释-genkeypair生成密钥对-keystore证书文件名这里生成的是myapp.keystore-alias别名代码里引用时要用-keyalg RSA -keysize 2048使用RSA算法密钥长度2048位-validity 10000有效期10000天执行后会提示输入密码、姓名、组织等信息这些信息务必保存好特别是密钥库密码和别名密码。上架后如果证书丢了应用就无法更新了只能换包名重新上架之前积累的用户和评分都会归零这是血淋淋的教训。在HBuilderX里打包时在“发行-原生App-云打包”界面选择“使用云端证书”或“使用本地证书”把本地证书路径、别名、密码填上即可。8.2 软件著作权申请热词里有人问“uniapp上架如何申请软著”这个确实绕不开。国内安卓应用市场华为、小米、OPPO、vivo、应用宝都要求提供软件著作权证书。我当时申请的是“点餐小程序管理系统”的软著提交到中国版权保护中心周期大概30-40个工作日可以加急。软著申请材料的核心是操作说明书和源代码文档操作说明书每个页面截图加上文字说明一般20-30页就够了源代码文档前后端代码的主要部分每页50行总共60页前后各30页。这里建议把核心代码Controller、Service、Mapper、前端页面整理成一个文档去掉空行注释保持代码可读性即可不需要提交全部代码。8.3 各应用市场的审核差异安卓应用市场特别多审核标准也不尽相同。华为、小米对隐私政策要求非常严格必须在小程序或App里明确展示隐私政策链接否则会被拒绝。OPPO、vivo对应用内“用户协议”是否涵盖账号注销功能、用户信息删除功能有明确要求。应用宝对App的targetSdkVersion有版本要求太低的会被直接拒绝。我踩过的一个坑是在manifest.json里配置了minSdkVersion: 21但某些市场的设备系统版本较低导致安装后无法打开。后来统一调整为minSdkVersion: 21、targetSdkVersion: 30既保证了兼容性又满足了市场要求。另外一个要提前准备的应用商店截图。各市场对截图尺寸要求不同但一般都要提供5张竖屏截图尺寸在1080x1920左右和1张横屏宣传图建议提前用模拟器在不同分辨率下截图避免临时抓瞎。8.4 后端上线部署的Spring Boot注意事项后端的部署相对简单我用的是阿里云轻量应用服务器宝塔面板Nginx反向代理。打包命令mvn clean package -DskipTests生成的target/dish-server.jar放到服务器上写一个启动脚本nohup java -jar dish-server.jar \ --spring.profiles.activeprod \ --server.port8080 \ --spring.datasource.urljdbc:mysql://localhost:3306/dish?useUnicodetruecharacterEncodingutf8mb4 \ --spring.datasource.usernameroot \ --spring.datasource.passwordxxxx \ logs/dish.log 21 nohup和让Java进程在后台持续运行即使SSH断开也不会终止。--spring.profiles.activeprod指定生产环境配置在application-prod.yml里配置生产环境的数据库连接、Redis地址等。这里有个经验之谈生产环境的数据库密码不要写在配置文件里直接提交到Git仓库要么用环境变量注入要么用配置中心管理。我用的是启动命令里通过--spring.datasource.password${DB_PASSWORD}方式引用环境变量这样即使配置文件泄露了也不会直接暴露数据库密码。Nginx反向代理的配置server { listen 443 ssl; server_name api.yourdomain.com; ssl_certificate /etc/nginx/cert/fullchain.pem; ssl_certificate_key /etc/nginx/cert/privkey.pem; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }微信小程序上线时必须使用HTTPS协议且端口是443这是硬性条件。开发阶段可以不校验域名直接请求HTTP接口但生产环境必须要HTTPS证书否则小程序无法正常请求后端接口。我就是用Nginx配置好了HTTPS反向代理才顺利通过微信小程序的审核。8.5 微信小程序审核的常见驳回原因微信小程序审核比安卓应用市场还要严格常见驳回原因有几个方向涉及餐饮类目需要提供食品经营许可证用户隐私保护指引要明确表示收集了哪些用户信息、具体用途如果小程序里有用户上传图片、评价等UGC功能需要增加内容审核机制诱导分享或者强制关注公众号等推广陷阱的也会被驳回。我遇到过一次驳回是因为“分享有礼”活动被判定为利益诱导分享被迫把分享加积分功能改成普通分享才过了审核。建议在功能设计阶段就避开这些红线。9. 从开发到上线的一个补充接口安全与防刷最后补充一点容易被忽略的东西——接口安全。点餐小程序对外开放了菜品列表、下单、支付回调等接口如果不对接口做基础防护很容易被恶意刷单或爬取数据。我做了几个简单有效的防护措施第一小程序端接口统一要求请求头携带Authorization字段值为用户在登录后获取的token。生成token的方式是用JWTJSON Web Token单点登录和鉴权都很方便而且无需在服务端存session天然适合无状态API设计。String token Jwts.builder() .setSubject(userId.toString()) .claim(openid, openid) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();第二配置拦截器统一校验tokenComponent public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行不需要登录的接口 String uri request.getRequestURI(); if (uri.contains(/login) || uri.contains(/category/list) || uri.contains(/dish/list)) { return true; } // 校验token String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BusinessException(401, 未登录); } try { Claims claims Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); request.setAttribute(userId, Long.parseLong(claims.getSubject())); return true; } catch (Exception e) { throw new BusinessException(401, 登录已过期); } } }第三下单接口增加频率限制同一用户30秒内最多提交1次订单防止用户快速重复提交同一订单String lockKey order:limit: userId; Boolean success redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(30)); if (Boolean.FALSE.equals(success)) { throw new BusinessException(500, 操作太频繁请稍后再试); }这里用Redis的setIfAbsentSETNX做分布式锁30秒后锁自动过期比直接判断订单表里有没有30秒内的订单要高效得多。订单表加unique约束或Redis锁都能防止重复下单但SETNX方案对大并发场景更友好。热词里有人问了“springboot解决pdf xss攻击”这个在点餐系统的订单详情或菜单报价单导出场景里也会遇到。核心思路就是过滤上传内容中的危险HTML标签和脚本比如白名单校验、禁止加载外部实体、对文件名做严格校验等。在Spring Boot里可以在全局异常处理器里统一拦截并转义危险字符或者在文件上传接口里校验文件类型和内容防止恶意代码进入服务器。还有“springboot增加swagger”——这个强烈建议加上特别是团队协作开发时。我用的是springfoxSpring Boot 2.7.x兼容3.0.0版本或springdoc-openapiSpring Boot 2.x使用springdoc-openapi-ui。简单配置后Swagger UI页面就能自动生成所有Controller的接口文档前端开发同学可以直接在页面上查看参数和返回值联调效率提升一倍不止。dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-ui/artifactId version1.7.0/version /dependency然后在启动类加一个OpenAPIDefinition注解配置基础信息访问/swagger-ui/index.html就能看到接口文档。上线前记得关闭Swagger避免暴露接口详情被人恶意调用。一点实际操作的体会这个项目最大的收获不是“会写Spring Boot代码”或者“会配置uniapp”而是理解了做一套完整产品的思路——从需求分析、技术选型、表结构设计、接口设计、前端开发、联调、测试、部署上线的完整闭环。每一步都有细节每一步都可能踩坑。上面这些坑都是真实的开发过程中一步步踩出来的如果你正在做类似的点餐小程序或者想从零开始做一个前后端分离的全栈项目希望对你有用。如果有其他问题欢迎在评论区交流。本文还有配套的精品资源点击获取
返回列表