微信小程序+Java后端鲜花商城实战:从数据库设计到前后端联调避坑
简介一款面向毕业设计与课程设计的鲜花销售系统资源包基于微信小程序与Java后端开发采用SSM框架和MySQL数据库覆盖管理员、商家、用户三种角色包含首页、个人中心、用户管理、商家管理、鲜花信息管理、鲜花分类管理、系统管理等核心功能结构完整且贴近实际业务场景。资源包共1341个文件约12.51MB包含Java源码、小程序WXML/WXSS页面、Vue后台页面、SQL脚本及部署说明其中PNG图片用于界面展示SVG与WXSS负责视觉样式JS与JSON承担业务逻辑和配置BAT脚本辅助完成环境安装与项目启动整体目录清晰便于按模块检索。目前已有235人学习下载适合计算机专业学生作为毕设选题参考或课程设计落地方案。资源内含完整源码、数据库及说明文档可直接运行并对照学习前后端交互、角色权限和SSM框架应用也可在此基础上扩展营销、订单等模块是快速完成项目实践的高性价比参考。1. 基于微信小程序java后端的鲜花销售毕业设计一套能答辩、能演示的完整交付“基于微信小程序java后端的鲜花销售毕业设计源码数据库说明.rar”标题听上去像一份打包好的作业实际覆盖了一条完整商城业务链路微信小程序负责展示鲜花、加购物车和下单Java后端提供登录、商品、订单接口MySQL把用户、鲜花、订单这些数据持久化下来。对准备答辩的毕业生来说这个题目最值钱的是凑齐了前端页面、后端接口、数据库设计、联调部署四件套做完能讲清楚前后端分离项目实战里的每一步。很多同学拿到压缩包后的第一反应是赶紧跑起来但真正卡住的往往不是代码而是端口没起、数据库账号不对、小程序里打开的是空模板。所以这篇文章不只讲源码里有什么还讲怎么把这一套从零搭到能演示、能答辩中间给出的SQL、Java接口和小程序登录代码都能直接抄末尾的避坑清单是实际联调最容易翻车的几个点。适合三类人正在做小程序商城类毕业设计的学生想做课程设计但不想从零写前后端的新手以及想复用登录、下单、库存扣减方案的一线开发者。2. 架构与技术选型前后端分离的鲜花商城为什么这套组合复现成本最低2.1 技术栈拆解Spring Boot、MyBatis Plus、MySQL各自的角色做小程序商城技术栈不是越新越好要的是“能被评委听懂、能被自己复现”。常见做法是后端用Spring Boot做Web框架用MyBatis Plus操作数据库数据库选MySQL。Spring Boot负责接收小程序发来的HTTP请求把参数绑定到Controller方法上MyBatis Plus负责把Java实体类和MySQL表做映射省掉大量手写JDBC的模板代码MySQL存业务数据。这套组合跑在本地只需要一台装了JDK8的电脑、一个MySQL实例、一个微信开发者工具完全不依赖Nacos、Redis这些中间件对毕业设计这种交付场景来说复现成本最低。有人会问为什么不用传统的JSPServlet或者更偏工程的Spring Cloud微服务原因很简单微信小程序是独立前端工程通过wx.request访问后端接口天然就是前后端分离的结构你没法用JSP直接渲染到小程序里而微服务对毕业设计来说纯粹是给答辩找麻烦分布式事务、注册中心这些概念一旦被问深很容易当场翻车。选Spring Boot加MyBatis Plus是在“功能完整”和“能讲清原理”之间的平衡点。登录、商品查询、下单、购物车四张业务表、五六个核心接口单体应用完全够用不要自己给自己上复杂度。开发环境上JDK建议用1.8Spring Boot选2.7.xMyBatis Plus选3.5.x这几个版本组合的资料最多报错时搜到的解决方案也最全。Java 17和Spring Boot 3也能跑但毕业设计没必要追新线上踩坑资料少一个问题可能卡掉半天。Maven配阿里云镜像源依赖下载会快很多。application.yml里配置端口、数据库连接、wx.appid和wx.secret四部分启动类上没有多余配置这就是最小的可运行后端。2.2 后端工程目录与分模块逻辑拿到源码先看什么拿到源码之后第一件事不是找Controller而是看目录结构。一个能顺利答辩的后端工程包名要清晰不能把所有类堆在一个包里。我一般会按common、config、entity、mapper、service、controller六层来分每一层只做一件事。src/main/java/com/example/flowershop ├── common │ ├── Result.java # 统一返回体 │ ├── JwtUtil.java # 登录token生成与校验 │ └── GlobalExceptionHandler.java # 全局异常处理 ├── config │ ├── WebMvcConfig.java # 拦截器与跨域配置 │ └── MybatisPlusConfig.java # 分页插件 ├── entity │ ├── User.java │ ├── Flower.java │ ├── Category.java │ ├── Cart.java │ └── Order.java ├── mapper │ ├── UserMapper.java │ ├── FlowerMapper.java │ └── OrderMapper.java ├── service │ ├── UserService.java │ ├── FlowerService.java │ └── OrderService.java └── controller ├── UserController.java ├── FlowerController.java ├── CartController.java └── OrderController.javacommon包放的是每个接口都要用的公共类比如统一返回体Result和JwtUtil。entity包下的类对应数据库里的表字段名和表的列名保持一致MyBatis Plus就能靠驼峰映射自动完成转换。mapper包是数据访问层大部分查询只需要写一个接口方法加一个注解复杂SQL再写到XML里。service包承担业务逻辑下单时的库存扣减、订单号生成都放这里。controller包只做参数接收和结果返回不允许写业务逻辑。这种分法的好处是答辩时被问到“订单逻辑在哪里”你能三秒钟指到OrderService.java这是最基础的加分项。2.3 统一返回体与接口路径约定前后端联调不吵架的前提小程序端拿到后端返回的数据最怕的是每个接口返回结构不一样。有的接口返回{code:0}有的返回{status:true}前端就得到处判空写出来的代码自己都嫌乱。解决这个问题主要靠统一返回体所有Controller方法都返回Result对象public class ResultT { private Integer code; // 200成功401未登录500业务失败 private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.message ok; r.data data; return r; } public static T ResultT error(String message) { ResultT r new Result(); r.code 500; r.message message; return r; } // getter/setter 略 }参数说明code全项目统一200表示成功401表示token过期或未登录前端收到401后清除本地token并跳转登录页500表示业务异常message里放人类可读的提示比如“库存不足剩余3件”。data字段放真正的业务数据可以是对象、列表也可以是空。有人习惯把成功定义成0也没问题但前端判断时要写对Java里Integer序列化出来是数字200小程序wx.request返回的res.statusCode是HTTP状态码两码事很多同学在这一点上栽过跟头。接口路径这块常见做法是以/api开头按资源名区分模块。登录接口POST /api/user/login商品列表GET /api/flower/list商品详情GET /api/flower/{id}下单POST /api/order/create订单列表GET /api/order/list。路径本身可以自由定义但前后端必须事先约定好最好在代码里留一份接口文档写清参数名、参数类型和是否必填。有些项目跑不起来不是逻辑问题而是后端返回的字段叫totalCount小程序里读的是total这类字段不一致的黑匣子问题会反复消耗联调时间。除了返回体和路径还有两个基础配置不能漏跨域和登录拦截器。小程序端的wx.request虽然不像浏览器一样强约束CORS但在H5预览和新版开发者工具下跨域问题仍会出现常见做法是在WebMvcConfig里注册CorsFilter允许所有来源和请求方法。拦截器方面写一个LoginInterceptor校验请求头里的Authorization登录和商品列表这类公开接口加PassAuth注解放行其余接口全部走token校验。这两个类放config包是后面每个接口能被小程序稳定调用的前提。3. 数据库设计从分类表到订单明细表把鲜花商城的数据结构一次建完3.1 核心表设计为什么是五张业务表加一张用户表鲜花销售小程序的数据模型不复杂但设计上要能自圆其说。常见做法是建六张表用户表user、分类表category、商品表flower、购物车表cart、订单主表orders、订单明细表order_item。用户表放微信openid、昵称、头像分类表放玫瑰、百合、向日葵这些类目商品表放价格和库存订单主表存收货人信息和订单总金额订单明细表存下单时刻的商品快照。用户表和订单表是一对多关系一个用户能下多笔订单。订单主表和订单明细是一对多关系一笔订单能包含多个商品比如“红玫瑰19支花束”和“向日葵单支”一起结算。为什么订单明细必须把商品名称、价格、图片复制一份而不是只存flower_id因为鲜花价格会变三个月后你查看历史订单如果直接关联商品表价格早就改了明细金额就对不上账。这是订单表设计里最常见的知识点答辩时主动讲出来是很自然的加分项。3.2 建表SQL商品表、购物车表、订单表与订单明细表在六张表里flower表和orders表最核心。flower表需要注意stock和status两个字段前者是库存后者是上下架状态。下单时判断库存是否充足扣减时用条件更新防止超卖这一块在后面接口章细讲。orders表里的status字段是订单状态机3.3单独说明。先看建表SQLCREATE TABLE category ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(32) NOT NULL COMMENT 分类名称, sort int(11) NOT NULL DEFAULT 0 COMMENT 排序权重越大越靠前, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT鲜花分类表; CREATE TABLE flower ( id bigint(20) NOT NULL AUTO_INCREMENT, category_id bigint(20) NOT NULL COMMENT 分类id关联category.id, name varchar(64) NOT NULL COMMENT 鲜花名称, price decimal(10,2) NOT NULL COMMENT 现价, original_price decimal(10,2) DEFAULT NULL COMMENT 划线价, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, sales int(11) NOT NULL DEFAULT 0 COMMENT 销量用于排序展示, main_image varchar(255) DEFAULT NULL COMMENT 商品主图URL, detail text COMMENT 富文本详情, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT鲜花商品表;这里有几个参数要说透。decimal(10,2)表示整数部分10位、小数2位存金额别用floatfloat在Java和MySQL里做等值比较会出现精度玄学问题。main_image存的是URL字符串不是二进制图片图片文件放static目录或对象存储数据库只记路径。stock默认0初始化数据时必须手动给够。status字段控制上下架下架的商品在小程序列表里不展示但历史订单明细里仍然要展示名称和价格所以商品表用status下架代替物理删除。CREATE TABLE orders ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号唯一, user_id bigint(20) NOT NULL COMMENT 用户id, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, receiver_name varchar(32) NOT NULL COMMENT 收货人, receiver_phone varchar(20) NOT NULL COMMENT 收货手机号, receiver_address varchar(255) NOT NULL COMMENT 收货地址, remark varchar(255) DEFAULT NULL COMMENT 订单备注, pay_time datetime DEFAULT NULL COMMENT 支付成功时间, deliver_time datetime DEFAULT NULL COMMENT 发货时间, finish_time datetime DEFAULT NULL COMMENT 完成时间, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; CREATE TABLE order_item ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id bigint(20) NOT NULL COMMENT 订单id, flower_id bigint(20) DEFAULT NULL COMMENT 商品id历史快照后可为空, flower_name varchar(64) NOT NULL COMMENT 商品名称快照, flower_image varchar(255) DEFAULT NULL COMMENT 商品图片快照, price decimal(10,2) NOT NULL COMMENT 成交单价快照, quantity int(11) NOT NULL DEFAULT 1 COMMENT 购买数量, subtotal decimal(10,2) NOT NULL COMMENT 小计金额, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表; CREATE TABLE cart ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, flower_id bigint(20) NOT NULL, quantity int(11) NOT NULL DEFAULT 1, checked tinyint(4) NOT NULL DEFAULT 1 COMMENT 1选中结算 0未选中, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_flower (user_id,flower_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT购物车表;订单明细表里flower_id允许为空是给自己留的后路商品下架甚至表数据被清理后历史订单照样能展示。order_no上加了唯一索引防止并发下重复订单号。购物车表和订单表不一样它只是用户结算前的临时容器表结构越简单越好uk_user_flower这个唯一索引保证同一个商品在同一用户的购物车里只有一行前端加购时调的是“数量加一”不是重复插入。3.3 订单状态机从待支付到已完成状态怎么流转才不会被问倒订单状态是一个完整的状态机用数字0到4对应五种状态。创建订单后是待支付支付成功后变已支付管理员发货后变已发货用户确认收货后变已完成。取消有两个入口用户在待支付状态主动取消变已取消超时未支付由定时任务或查询时判断被动置为已取消。状态流转必须做限制不能从已支付直接跳已完成。最简单的约束方案是更新语句带上期望的前置状态比如“UPDATE orders SET status1 WHERE id? AND status0”这种条件更新既把状态机约束落到数据库层又天然防了重复支付回调。答辩讲状态机时用“每笔订单在任何时刻只能处于一个状态状态迁移方向固定”这句话开场再结合pay_time、deliver_time、finish_time三个时间字段解释边界条件。毕业设计大多不接真实微信支付但模拟支付也要走同样的流转逻辑否则后面想接真支付改动会非常大。3.4 初始化数据鲜花分类和演示商品怎么灌进去源码包的数据库目录下一般会带一个init.sql或flowershop.sql。如果手头这份没有自己手工执行下面的语句也够用。初始化数据的作用是让答辩现场一打开小程序就能看到商品而不是对着空列表讲逻辑。建议每个分类造两三条商品价格有梯度比如19.9的一支装、129的花束、399的礼盒小程序端能看到卡片展示和价格排序的效果。INSERT INTO category (name, sort) VALUES (玫瑰, 1), (百合, 2), (向日葵, 3), (绿植盆栽, 4); INSERT INTO flower (category_id, name, price, original_price, stock, sales, main_image, status) VALUES (1, 红玫瑰19支花束, 129.00, 169.00, 100, 200, /images/rose.png, 1), (1, 白玫瑰礼盒, 159.00, 199.00, 80, 120, /images/whiterose.png, 1), (2, 多头百合花束, 99.00, 129.00, 60, 80, /images/lily.png, 1), (3, 向日葵单支, 19.90, 29.90, 200, 400, /images/sunflower.png, 1), (4, 茉莉盆栽, 49.90, 69.90, 50, 35, /images/jasmine.png, 1);SQL里的main_image是相对路径需要把对应图片放到后端项目的static/images目录下或者替换成可访问的URL。本地联调时微信开发者工具里勾选“不校验合法域名”可以用http://localhost:8080访问真机预览时必须用已备案、带HTTPS证书的域名。把数据先灌进去后续接口才有真实的调试对象否则每次都要手造JSON联调效率极低。4. 后端接口实现登录、商品列表、下单与库存扣减的完整写法4.1 微信登录与token签发openid换session后JWT怎么生成微信小程序登录的标准流程是小程序端调wx.login拿一个临时code把code发给自己的后端后端用code去微信jscode2session接口换openid和session_key再用openid查用户表用户不存在就创建存在就更新信息最后生成token返回给小程序之后每次请求都在Header带上这个token。public class UserServiceImpl implements UserService { Value(${wx.appid}) private String appid; Value(${wx.secret}) private String secret; Override public String login(String code) { String url https://api.weixin.qq.com/sns/jscode2session ?appid appid secret secret js_code code grant_typeauthorization_code; String respJson HttpUtil.get(url); // 用OkHttp或RestTemplate发起GET JSONObject resp JSON.parseObject(respJson); String openid resp.getString(openid); if (openid null) { throw new BizException(code已失效请重新wx.login); } User user userMapper.selectOne( new LambdaQueryWrapperUser().eq(User::getOpenid, openid)); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(微信用户); user.setAvatar(default.png); userMapper.insert(user); } return JwtUtil.generateToken(user.getId(), user.getOpenid()); } }代码逻辑说明jscode2session是微信官方接口传appid、secret、js_code、grant_type四个参数返回JSON里取openid。session_key是解密手机号、运动数据用的当前登录场景拿到openid即可不要把它当业务token返回前端。第三步用MyBatis Plus的LambdaQueryWrapper按openid查用户列名用方法引用而不是字符串重构时不容易出错。用户首次创建时昵称给“微信用户”头像给默认图因为wx.login本身拿不到昵称头像只有用户用open-type组件授权后前端才会把真实资料传过来。最后生成的JWT把用户id和openid放进去过期时间设7天后端拦截器解析JWT并把用户id放入request上下文。4.2 商品与分类接口小程序首页到底需要哪几个接口小程序首页通常需要三个数据轮播图、分类tab、当前分类下的商品列表。轮播图可以建banner表但毕业设计更常见的做法是用小程序的swiper组件配静态图片少一张表少一个接口。分类和商品列表走两个接口GET /api/category/list返回全部分类GET /api/flower/list支持按categoryId和keyword筛选。RestController RequestMapping(/api/flower) public class FlowerController { Autowired private FlowerService flowerService; GetMapping(/list) public ResultPageResultFlower list( RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword) { PageFlower result flowerService.pageQuery(page, size, categoryId, keyword); return Result.ok(PageResult.of(result)); } GetMapping(/{id}) public ResultFlower detail(PathVariable Long id) { Flower flower flowerService.getById(id); if (flower null || flower.getStatus() 0) { throw new BizException(商品不存在或已下架); } return Result.ok(flower); } }参数说明page和size实现分页默认第1页每页10条小程序触底时page加1继续请求。categoryId可选传了就按分类过滤不传就返回全部上架商品。keyword是搜索关键字按名称模糊匹配。detail接口查详情后判断status下架商品不能展示。分页用MyBatis Plus的Page对象配合PaginationInnerInterceptor就能工作不需要手写LIMIT。接口写完可以在浏览器直接访问http://localhost:8080/api/flower/list?page1size10验证返回JSON再决定要不要动前端页面。4.3 下单接口事务加条件更新把超卖问题扼杀在SQL里下单是整个项目里最容易出bug的地方核心问题是库存扣减和订单生成的原子性。如果先查库存再update两步之间出现并发窗口两个人同时买最后一支向日葵就会超卖。常见做法是直接在UPDATE语句里带stock quantity条件影响行数是1才说明扣减成功然后用Transactional把库存扣减、订单主表插入、订单明细插入包在同一个事务里任何一个失败都回滚。Transactional(rollbackFor Exception.class) public Order createOrder(Long userId, ListCartItem items, Address address) { BigDecimal total BigDecimal.ZERO; String orderNo FL System.currentTimeMillis() userId; Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); for (CartItem item : items) { int rows flowerMapper.deductStock(item.getFlowerId(), item.getQuantity()); if (rows 0) { throw new BizException(库存不足商品id item.getFlowerId()); } OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setFlowerName(item.getFlowerName()); oi.setPrice(item.getPrice()); oi.setQuantity(item.getQuantity()); orderItemMapper.insert(oi); } cartMapper.deleteByUserIdAndFlowerIds(userId, itemIds); return order; }update iddeductStock UPDATE flower SET stock stock - #{quantity}, sales sales #{quantity} WHERE id #{flowerId} AND stock #{quantity} /update这段设计的要点有三个。第一Transactional写在public方法上且不能被同类内部方法调用否则事务失效这是典型的翻车点。第二deductStock影响行数判断扣减是否成功等于0抛异常回滚不会出现订单成功但库存没扣的情况。第三订单明细里存的flowerName、price是下单当时的快照之后商品表改价不影响历史订单。收货地址让用户在小程序端填表单后端存到receiver_phone和receiver_address字段不要依赖微信的收货地址接口那个接口需要企业资质个人开发者调不通。4.4 模拟支付与订单取消不加真实微信支付时状态怎么流转毕业设计里九成不会接真实微信支付因为商户号要企业资质。更稳妥的做法是做“模拟支付”按钮点击后调后端POST /api/order/pay接口把这个订单从待支付改成已支付。逻辑不复杂但防御要到位支付接口必须校验订单属于当前用户、必须校验订单状态是0、更新语句里带原状态条件。PostMapping(/pay) public ResultString pay(RequestParam Long orderId) { Order order orderMapper.selectById(orderId); if (order null || !order.getUserId().equals(currentUserId())) { throw new BizException(订单不存在); } int rows orderMapper.updateStatus(orderId, 1, 0, new Date()); if (rows 0) { throw new BizException(当前状态不可支付); } return Result.ok(支付成功); }updateStatus的SQL大致是UPDATE orders SET status1, pay_time#{now} WHERE id#{orderId} AND status0。带上原状态条件用户连点两次模拟支付时第一次成功改成1第二次进来影响行数为0直接提示“当前状态不可支付”天然实现了幂等。订单取消接口同样思路把期望状态从0改成4只允许待支付订单取消。答辩时老师问“为什么不用先查再改”你可以说条件更新是数据库层的原子操作比先查后改更可靠这句话本身就是加分回答。5. 微信小程序端联调避坑登录态、导航栏与支付模拟的5个高频问题5.1 小程序端登录与请求封装request方法统一处理token和401后端接口就绪后小程序端需要一个统一的请求封装。最省事的方案是写一个utils/request.js把BASE_URL、请求头、成功判断都收进去。这样页面里下单、加购、查订单只需要调用request(/api/order/list)不用每个页面都重复处理token过期逻辑。// utils/request.js const BASE_URL http://localhost:8080 function request(path, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || }, success(res) { if (res.data.code 200) { resolve(res.data.data) } else if (res.data.code 401) { wx.removeStorageSync(token) wx.navigateTo({ url: /pages/login/login }) reject(new Error(登录已过期)) } else { wx.showToast({ title: res.data.message, icon: none }) reject(new Error(res.data.message)) } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) } module.exports { request, BASE_URL }逻辑说明request方法把token统一放进header里的Authorization字段和后端LoginInterceptor约定一致。业务码判断用res.data.code而不是HTTP状态码。401时清token并跳登录页其他非200错误用wx.showToast给用户提示。BASE_URL在本地联调是localhost部署时改成HTTPS域名。参数method支持GET和POST但wx.request的GET请求data会被拼接成query stringPOST请求data放body后端用RequestParam或RequestBody接收时要对应。小程序端调用登录的代码就三行wx.login拿coderequest发POST把返回的token存到Storage。wx.login({ success(res) { request(/api/user/login, POST, { code: res.code }).then(token { wx.setStorageSync(token, token) }) } })这块逻辑虽然短但要注意code只能用一次不能缓存在本地反复用。每次进入小程序前台都重新wx.login一次后端换openid是最稳妥的登录姿势。5.2 顶部导航栏高度与安全区不同机型为什么页面会顶出去小程序页面被iPhone刘海屏顶出去是答辩演示时最容易当场翻车的问题。原因是自定义导航栏时顶部固定高度写死了44px或64px但不同机型的状态栏高度不一样Android一般24pxiPhone 13是47px折叠屏还可能更高。正确做法是运行时动态计算状态栏高度和胶囊按钮位置再撑起自定义导航栏高度。const windowInfo wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() const menuRect wx.getMenuButtonBoundingClientRect() const statusBarHeight windowInfo.statusBarHeight const navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.height this.setData({ statusBarHeight, navBarHeight })逻辑说明wx.getMenuButtonBoundingClientRect返回胶囊按钮的top和height状态栏到胶囊顶部之间的距离乘以2再加上胶囊高度就是自定义导航栏的标准高度。这段代码放在每个页面的onLoad里执行然后把statusBarHeight和navBarHeight绑定到WXML的占位view上。如果用的是原生导航栏不需要处理这个但很多毕业设计为了视觉效果会用自定义导航栏这个计算就必须做。真机演示前至少用iPhone 13和一台普通Android各测一遍别在答辩现场临时调样式。5.3 高频踩坑记录按现象、原因、解决三步定位这一节把我做小程序商城联调时遇过的高频问题按“现象、原因、解决”整理出来每一条都是血泪经验。第一开发者工具里接口正常真机预览全部请求失败。原因是真机要求HTTPS和备案域名开发者工具勾了“不校验合法域名”只在工具内有效。解决方法是把后端部署到有HTTPS证书的服务器在小程序后台配置request合法域名再重新预览。第二wx.login的code有时候换openid失败。原因是code只能用一次且五分钟内有效前端如果缓存了旧code就会失效。解决方法是每次进入页面都重新wx.login不要复用Storage里的code。第三手机号获取按钮点了没反应。原因是getPhoneNumber接口要求企业主体且有相应权限个人主体小程序拿不到这个能力。解决方法是改成表单输入手机号配合一个模拟绑定流程。答辩时主动提“个人主体没有获取手机号权限这里用表单替代真实绑定”反而能体现你了解业务边界。第四iPhone刘海屏页面顶到状态栏内容被遮挡。原因是自定义导航栏高度写死。解决方法是按5.2里动态计算高度不要用固定px。第五Java后端BigDecimal金额传到小程序后变成字符串结算总价算出NaN。原因是Jackson默认把BigDecimal序列化成字符串你拿字符串做加法肯定出问题。解决方法有两种后端配置ObjectMapper把BigDecimal转Number或者前端用Number()把金额字段转成数字再计算。我建议后端转Number因为前端如果不止一个页面用到金额统一处理少踩坑。金额展示保留两位小数用toFixed(2)格式化不要在多个页面自己拼。6. 答辩验收把鲜花小程序从能跑到能讲清的三步收尾6.1 本地启动顺序MySQL、后端、小程序三端按什么顺序拉起答辩前一天把启动顺序固定下来能省一半时间。第一步启动MySQL并用Navicat执行数据库脚本确认flower和orders表都在第二步启动Spring Boot后端看到“Started FlowerShopApplication”后访问一下http://localhost:8080/api/flower/list确认接口能返回JSON第三步打开微信开发者工具导入小程序前端工程在详情里勾选“不校验合法域名”appid换成测试号或自己的小程序appid。这个顺序不能乱先起后端再开小程序联调路径最短。mysql -uroot -p flowershop.sql mvn spring-boot:run如果数据库脚本执行报错多半是字符集问题把SQL文件另存为utf8mb4编码再执行。后端启动失败先看application.yml里的数据库账号密码和端口有没有被占用。小程序编译报错优先看appid是否配置。6.2 答辩演示路径与老师常问的三个问题演示路径建议固定成一条主线进入小程序首页看分类和商品列表点进商品详情加购进购物车选中结算填收货地址下单点模拟支付再进订单列表看到状态从待支付变已支付。这条线覆盖了表、接口、状态机三个核心考点。老师常问的问题也有固定打法问技术栈就答Spring Boot加MyBatis Plus强调单体应用减少部署成本问超卖就答条件更新加事务结合SQL影响行数解释问token就答JWT无状态拦截器校验7天过期策略。如果不提前走一遍这条路径答辩现场很容易卡在某个细节比如库存不够、端口被占用、token过期。我自己当年就是这样答完原理很顺一到演示就被一台老Android的安全区顶掉了自定义导航栏页面看起来像是没做完。所以现在做这类小程序项目我养成的习惯是答辩前一晚从数据库初始化开始完整跑三遍每跑一遍记录一次异常。希望这份从架构到避坑的梳理能帮到你照着把数据、接口、页面三层串起来毕业设计这件事就稳了。本文还有配套的精品资源点击获取